Prelude
First of all, thank you for your time and for your patience.
I know that when I was in school I never wanted to read a book and, except for The Cay and Great Expectations, I got away without having to.
So while it is hypocritical for me to ask this of you, please read this book like a book and go from the start to the end, one section at a time.
At the end of each section there will also be challenges. I want you to at least attempt all of them before moving on to the next section.
This book is written specifically for those folks that feel like giving up, like they are too stupid to get it, or like they didn't understand a damn thing their professor has said for the last three months.
I see you. You are not stupid. This is not your fault.1
probably.
Asking for Help
If anything in this confuses you or feels intimidating please, please reach out to someone.
If you are in a position to, make use of your teacher. That is what they are there for.
If you are not, on every page there is a button in the top right corner which you can click to ask a question.
When you do, keep in mind these basic rules.
1. You get what you pay for
Often the people willing to help you will be volunteers.
Do not expect them to do your assignments for you, do not expect them to be available at a moments notice, and accept that they are under no obligation to help anyone or you in particular.
2. Ask early
If you are struggling with something and you have a deadline in 5 hours, that might not be enough time to do anything.
Asking questions well before a deadline is best for all those involved.
3. Don't Ask to Ask
Don't ask to ask, nor only say hello, just ask your actual question right away.
Consult https://dontasktoask.com/ for what that means exactly.
Toy Problems
As you go through this book there will be a lot of what can be considered "toy problems."
That is, you will be shown or asked to write programs that have no obvious real world use. They will be similar in spirit to math problems like "if you have 8 apples and I take 3, how many apples do you have left?"
The intent is not for these to be annoying, although I am sure they will be. The intent is for these problems to help you build an understanding of mechanics and structure.
Lies
At various points in this book I am also going to lie to you. Specifically I will make lies of omission.
If there are five ways to do something, I might pretend that there is only one way to do it until I give you enough context so that I can talk about the other four.
This can be particularly annoying if you are in a course that is teaching things in a different order or if you feel rushed to just get some project done.
I get that. I just ask that you trust me and go in the sequence I am laying out. I am honestly trying to make it so that you end with a stronger understanding than you would have otherwise.
There is the question mark in the top right of every page.
Getting Started
There are a lot of ways to "get set up" to run Java code.
For at least the first chunk of this, you should be able to get away with using the editor on https://run.mccue.dev. That might be the easiest.
I will add tutorials here as they are requested or as I have time, but for the start all that matters is that you have the ability to run and edit the following code.
void main() {
System.out.println("Hello, World");
}
Windows
Download the "JDK MSI" from adoptium.net.
Run the installer, selecting all the default options.
Mac OS
Download the "JDK .pkg" from adoptium.net.
Run the installer, selecting all the default options.
Linux
Linux is a little annoying. If you are using it you are likely used to it by now, but you can use adoptium.net like everyone else, but there is no universal installer there.
You can either download the .tar.gz
file that matches your machine, extract it,
and add the bin
folder to your PATH
, or you can try to find an installer for your
specific linux distribution.
repl.it
replit.com is a pretty common choice for teachers because they will be able to give you assignments and have you share back your results. It is also a decent option if your school only provides you with Chromebooks or similar.
It requires an internet connection and you will have to make an account, but otherwise it is fairly convenient.
If you are in school and your teacher has helped you get set up in some other way it is okay to skip this section and just do it the way you were shown.
Step 1. Make an account
Go to replit.com and find the "Sign Up" button. Websites change every now and then so these screenshots might be out of date.
Click it and sign up for an account.
Step 2. Create a Java REPL
Find the "Create REPL" button and click it.
Then you should be presented with a menu that lets you search for the type of REPL to create. Find the Java template and click "Create".
Step 3. Run code
You should land on a screen with a big green run button, an open file called "Main.java", and a blank window labeled "console".
Click that run button, and you should see the text Hello, world!
appear under the console window.
First Steps
If you made it through the Getting Started section you've successfully run this program.
void main() {
System.out.println("Hello, World!");
}
This "prints" - not in the sense of a physical printer, but like "displays on the screen" -
the text "Hello, World!"
.
Its a tradition for this to be your first program in any language.
We aren't quite at the point where I can explain what void main()
means, but
all you need to know for now is that it is what Java looks for to know where to start the program.
void main() {
< WRITE YOUR CODE HERE >
}
So for all intents and purposes, this is the whole program.
void main() {
System.out.println("Hello, World!");
}
This bit of magic here - System.out.println
- is a "statement" that "prints" the text inside the (
and )
as well as a "new line" to the screen.
print with new line.
If you were to replace it with System.out.print
, then the output would lack that new line. This makes the following program be functionally identical to the first.
void main() {
System.out.print("Hello, ");
System.out.print("World");
System.out.println("!");
}
Which, when we add back void main()
, looks like this.
void main() {
System.out.print("Hello, ");
System.out.print("World");
System.out.println("!");
}
You should get in the habit of, whenever you see some bit of code, trying to physically type it out, run it, tweak it, and play with it.
So try playing around with this program. If you actively engage then you will retain information better.
Comments
At various points, I am going to leave "comments" in the code. Comments are parts of the code that are solely there for a human to be able to read as an explanation and can be written in regular words.
Single-line Comments
void main() {
// This prints hello world!
System.out.println("Hello, World!");
}
The rules for this are that if you see a //
, everything after that in the same line
is ignored.
If you put //
in front of something that is "code" and not an English explanation we colloquially call that "commenting out" the line.
void main() {
System.out.println("Hello, World!");
// The line that prints out goodbye is "commented out"
// System.out.println("Goodbye!");
}
You might want to do that at various points where you want to see what happens if you "turn off" parts of the code.
Multi-line comments
If you put /*
in the code then everything up until the next */
will be treated as a comment. The distinction
here is that this style of comment can span multiple lines.
void main() {
/*
I have eaten
the plums
that were in
the icebox
and which
you were probably
saving
for breakfast
Forgive me
they were delicious
so sweet
and so cold
*/
System.out.println("Hello, World!");
}
So that's a mechanism you will see me use and you can use yourself however you see fit.
Semicolons
The ;
at the end of each of those lines is a "semicolon".
void main(){
System.out.print("Hello, "); // <-- this thing
// ^
}
It indicates that the statement has finished. A "statement" is a line of code that "does something." The reason we call it a statement and not just a "line of code" is that it can technically span multiple lines and be more complicated than these examples.
void main(){
System.out.print(
"Hello, "
);
}
The ;
at the end is needed so that Java knows that the statement is over.
You need to put a ;
at the end of every statement. If you do not, Java will get confused and your code will not work.
If you happen to have an extra semi-colon or two that is technically okay. It just reads it as an "empty statement." It's pointless, but it is allowed.
void main() {
System.out.print(
"Hello, "
);;
}
Or even
void main() {
System.out.print(
"Hello, "
);
// Technically legal, but kinda sus
;;;;;;;;;;;;;
;;; ;;
;;; ;;
;;;;;;;;;;;;;
;; ;;; ;;
;;;;;;;;;;;;;
; ; ; ;
; ; ; ;
;;; ;;;
}
Formatting
You may have noticed that after each {
all the code that comes after it is "indented" in one "level."
void main() {
System.out.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() {
System.out.println("Hello, World!");
}
And not like this.
❌
void main()
{
System.out.println("Hello, World!");}
And keep in mind that this rule of thumb applies to every language construct that requires a {
and }
many of which I will introduce later.
Challenges
At the end of each larger section, I am going to write down some things you can do to make sure you get what was just gone over.
The rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write a program that prints your name twice. So if your name is "Jasmine", the output of the program should be this.
Jasmine
Jasmine
Challenge 2
What will this program output when run? Write down your guess and then try actually running it.
void main() {
System.out.println("A");
//System.out.println("B");
System.out.println("C");//
System.out.println("D");
/*
System.out.println("E");
System.out.println("F");*/
System.out.println("G");
}
Challenge 3
There are four semicolons in this perfectly functional program. Delete one of them at random and see what the errors you get look like.
How could you use that error to figure out where you might have forgotten to put a semicolon?
void main() {
System.out.println("Apple");
System.out.println("Banana");
System.out.println("Clementine");
System.out.println("Durian");
}
Local Variables
Mechanically, the next thing to cover is "variables".
void main() {
String boss = "Jaqueline";
System.out.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 System.out.println("Jaqueline");
String boss = "Jaqueline";
System.out.println(boss);
}
Naming
It is a social convention1 that local variables be named likeThis
.
That is if their name is one word, that word should be in lowercase.
String apple = "Red Delicious";
If it is multiple words, the first word should be lowercase and the others should start with a capital letter.
This convention is called camelCase
because the capitals looks like the humps on a Camels back.
Just like proper formatting, sticking to this style will increase your chances of someone online being able to help you with your code.
Reassignment
After a variable is declared and assigned an initial value, that value can be later reassigned.
void main() {
String boss = "Jaqueline";
System.out.println(boss);
boss = "Chelsea";
System.out.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"
System.out.println(boss);
boss = "Chelsea";
// But this will output "Chelsea"
System.out.println(boss);
}
Delayed Assignment
The declaration of a variable and the assignment of its initial value can be done separately.
void main() {
String contestWinner;
contestWinner = "Boaty McBoatface";
System.out.println(contestWinner);
}
In which case the "variable declaration" will only have the "type" and "name" components.
String contestWinner;
// type name
And the "initial assignment" will look identical to a "re-assignment".
contestWinner = "Boaty McBoatface";
// name initial value
Before an initial value is assigned to a variable, it is not allowed to be used.1
void main() {
String contestWinner;
// This will not run, since Java knows that
// you never gave contestWinner an initial value.
System.out.println(contestWinner);
}
There is no direct use for separating declaration and initial assignment at this point, but it's a surprise tool that will help us later.
Types
When a variable is declared, it is given a type.
String color = "green";
In this case, the variable color
is declared to have the type String
.
After this declaration, color
cannot be assigned to a value that is not a String
.
// A number is not a String!
String color = 8;
This applies to all situations where a variable might be given a value, including delayed assignment and reassignment.
One mental model is that types are like shapes. If the type of something is a circle, you can only put circles into it.
◯ thing = ◯;
You cannot put square pegs in that round hole.
// If Java actually functioned in terms of shapes, this
// would not work since a Square is not the same "type"
// of thing as a Circle.
◯ thing = ▢;
Final Variables
There is an optional extra part to a variable declaration where you can mark a variable as "final", meaning its value can never be reassigned.
void main() {
final String coolestChef = "Anthony Bourdain";
System.out.println(coolestChef);
}
If you try to reassign a final variable, Java will not accept your program.
void main() {
final String coolestChef = "Anthony Bourdain";
System.out.println(coolestChef);
// I'm sorry, but no. Cool guy, but no.
coolestChef = "Gordan Ramsey";
System.out.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"
System.out.println(genie);
Variables whose assignment is delayed can also be marked final.
void main() {
final String mario;
mario = "Charles Martinet";
System.out.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";
System.out.println(mario);
}
The downside to this, of course, is more visual noise. If a variable is only
"alive" for a small part of the code, then adding final
might make it harder
to read the code, not easier.
Inferred Types
In many cases, Java is smart enough to know what the type of a variable should be
based on what it is initially assigned to.
In these cases, you can write var
in place of the type and let java "infer" what it should
be.
void main() {
// Since what is to the right hand side
// of the = is in quotes, Java knows that
// it is a String.
var theDude = "Lebowski";
System.out.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";
System.out.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";
System.out.println(theDude);
}
Important to note that even if Java is smart enough to automatically know the type, you might not be yet. There is no shame in writing out the type explicitly.
void main() {
String theDude = "lebowski";
System.out.println(theDude);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
String mascot = "The Noid";
System.out.println(mascot);
mascot = "Pizza the Hut";
System.out.println(mascot);
mascot = "Little Caesar";
System.out.println(mascot);
}
Challenge 2
Why won't this code run? Make it run by only changing one line.
void main() {
String fruit;
fruit = "apple";
System.out.println(fruit);
final String vegetable = "carrot";
System.out.println(fruit);
System.out.println(vegetable);
fruit = "orange";
vegetable = "celery";
System.out.println(fruit);
System.out.println(vegetable);
}
Challenge 3
What is the output of this code?
void main() {
String a = "A";
String b = "B";
b = a;
a = b;
b = a;
a = b;
System.out.println(a);
System.out.println(b);
}
Challenge 4
Only adding lines in the middle and without writing "A"
or "B"
again,
make it so that the output of the program is
B
A
void main() {
String a = "A";
String b = "B";
// Don't touch above this
// You can add code here
// Don't touch below this
System.out.println(a);
System.out.println(b);
}
Challenge 5
Some of the variables in this program are named "wrong."1 Fix them.
void main() {
String apple = "red";
String clown_car = "polka dot";
String SeriousCar = "black";
String FASTRunner = "bolt";
String slowRunner = "tortoise";
}
By currently prevalent social conventions. None are actually "wrong" from the perspective of Java.
Booleans
A boolean
is either true
or false
.
boolean onFleek = true;
boolean badVibes = false;
This is used to represent situations where there are exactly two possible states.
Not
Booleans can also be "negated" using the "not" operator - !
.
boolean haveOreosInHouse = true;
boolean stuckToCalorieLimit = !haveOreos;
So in this case, I have stuck to my calorie limit if there are not Oreos in the house.
haveOreosInHouse | stuckToCalorieLimit |
---|---|
false | true |
true | false |
And
One way multiple booleans can be combined is by using the "and" operator - &&
.
boolean funToBeAround = true;
boolean believesInFundamentalHumanRights = true;
boolean willAskOnDate = funToBeAround && believesInFundamentalHumanRights;
So in this case, I will ask someone on a date if they are fun to be around and they wholeheartedly believe in the assertions made in the Universal Declaration of Human Rights.
funToBeAround | believesInFundamentalHumanRights | willAskOnDate |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
Or
Another way booleans can be combined is by using the "or" operator - ||
.
boolean dogLooksNice = true;
boolean personLooksNice = false;
boolean willAskToPetDog = dogLooksNice || personLooksNice;
So in this case, I will ask to pet someone's dog if either the the dog looks nice or the person walking the dog looks nice.
dogLooksNice | personLooksNice | willAskToPetDog |
---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
Exclusive vs. Inclusive
It is important too note that this is not an "exclusive" OR.
An exclusive OR would be something like a child being allowed to have ice cream or a cookie, but not both.
The ||
operator is an "inclusive" OR, meaning the child is allowed ice cream, a cookie, or both ice cream and the cookie.
Operator Precedence
The operators that work on booleans have a "precedence order."
This is defines an order of operations similar to mathematics, where multiplication and division happen before addition and subtraction.
For booleans !
always happens first. This is followed by &&
and then by ||
.
boolean a = true;
boolean b = false;
boolean c = false;
// just as 2 + 5 * 3 "evaluates" 5 * 3 before adding 2
// first, !b is true
// second, a && true is true
// third true || c is true.
boolean result = a && !b || c;
Also like mathematics, parentheses can be used to control this order.
// Even though || has a lower precedence than &&, we evaluate
// !b || c first because of the parentheses.
boolean result = a && (!b || c);
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
boolean a = true;
boolean b = false;
boolean c = true;
boolean d = false;
boolean result = a || b && c || !d;
System.out.println(result);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
boolean a = true;
boolean b = false;
boolean c = true;
boolean d = false;
boolean result = !(a || b && c || !d) || (a && b || c);
System.out.println(result);
}
Challenge 3
Say you have two boolean variables, how could you use the operators we've covered to get the "exclusive or" of the two.
void main() {
// Change these two variables to test your solution
boolean hasIceCream = true;
boolean hasCookie = false;
boolean validChoice = < YOUR CODE HERE >;
System.out.println(validChoice);
}
Make sure to test all the possibilities.
hasIceCream | hasCookie | validChoice |
---|---|---|
true | true | false |
true | false | true |
false | true | true |
false | false | false |
Integers
An integer is any number in the set { ..., -2, -1, 0, 1, 2, ... }.
int x = 1;
int y = 8;
int z = -4;
Integer Literals
In order to write an integer in a program, you write an "integer literal."
789
We call them this because the integer is literally written down in the program.
int trueCrime = 789;
Addition
You can add any two int
s using the +
operator.
void main() {
int x = 5;
// y will be 6
int y = x + 1;
// z will be 11
int z = x + y;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Adding a negative number does the same thing as subtraction.
void main() {
int x = 5;
// y will be 1
int y = x + -4;
System.out.println(x);
System.out.println(y);
}
Subtraction
You can subtract any two int
s using the -
operator.
void main() {
int x = 5;
// y will be 4
int y = x - 1;
// z will be 1
int z = x - y;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Subtracting a negative number does the same thing as addition.
void main() {
int x = 5;
// y will be 9
int y = x - -4;
System.out.println(x);
System.out.println(y);
}
Multiplication
You can multiply any two int
s using the *
operator.
void main() {
// x will be 15
int x = 3 * 5;
// y will be 75
int y = x * 5;
// z will be 1125
int z = x * y;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Division
You can divide any two int
s using the /
operator.
void main() {
int x = 8;
// y will be 4
int y = x / 2;
System.out.println(x);
System.out.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;
System.out.println(x);
System.out.println(y);
}
Remainder
To get the remainder of the division between two integers you can use the %
operator.
This is called the "modulo operator."
void main() {
int x = 5;
// The remainder of 5 / 2 is 1
// y will be 1
int y = x % 2;
// The remainder of 5 / 3 is 2
// z will be 2
int z = x % 3;
System.out.println(x);
System.out.println(y);
System.out.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;
System.out.println(value);
// the remainder of (0 + 1) divided by 3 is 1
// value will be 1
value = (value + 1) % 3;
System.out.println(value);
// the remainder of (1 + 1) divided by 3 is 2
// value will be 2
value = (value + 1) % 3;
System.out.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;
System.out.println(value);
// the remainder of (0 + 1) divided by 3 is 1
// value will be 1
value = (value + 1) % 3;
System.out.println(value);
// and so on.
//
// If you did this process with 5 you would go
// 0, 1, 2, 3, 4, 0, 1, ...
//
// If you did this process with 7 you would go
// 0, 1, 2, 3, 4, 5, 6, 0, 1, ...
//
// You always go back to the start just before you reach
// the number you are getting the remainder by.
}
The fact that all the reassignments of value look identical is something that will be useful in tandem with loops.
Equality
Any two int
s can be inspected to see if their value is equal by using the ==
operator.
Unlike the previous operators, which all take int
s and produce int
s as their result, ==
takes two int
s
and produces a boolean
as its result.
void main() {
// 1 is never equal to 2
// this will be false
boolean universeBroken = 1 == 2;
System.out.println(universeBroken);
int loneliestNumber = 1;
int canBeAsBadAsOne = 2;
// this will be true
boolean bothLonely = loneliestNumber == (canBeAsBadAsOne - 1);
System.out.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;
System.out.println(universeOkay);
}
Comparison
In addition to comparing for equality with ==
and !=
, int
s can be compared to see if one is bigger than another using
>
, <
, >=
, and <=
.
>
will evaluate to true if the number on the left is greater than the one on the right.
boolean willBeTrue = 5 > 2;
boolean willBeFalse = 2 > 5;
<
will evaluate to true if the number on the right is greater than the one on the left.
boolean willBeFalse = 5 < 2;
boolean willBeTrue = 2 < 5;
If you went to public school like I did, you should be used to imagining that the >
was the jaw of a shark.
Whatever direction the Jaws are facing, thats the one that would need to be bigger for the statement to be true.1
// true if the shark is facing the bigger number
// false otherwise.
boolean result = 9 🦈 5;
>=
behaves the same as >
except >=
will evaluate to true
if the numbers are identical, >
will not.
boolean willBeTrue = 5 >= 5;
boolean willBeFalse = 5 > 5;
<=
has the same relationship to <
as >=
does to >
.
boolean willBeTrue = 5 <= 5;
boolean willBeFalse = 5 < 5;
Shark attacks are far more rare than people think they are. You are not a seal.
Chained Comparisons
When writing an expression in math to say something along the lines of
"x
is greater than zero and less than 5," it is natural to put that x
in the middle of both operators like so.
0 < x < 5
This does not work in Java. In order to "chain" comparisons like this, you should combine
the results of comparisons using the &&
operator.
boolean xInRange = 0 < x && x < 5;
Operator Precedence
Just like boolean operators, +
, -
, *
, /
, and %
have a defined precedence order.
The order of operations is the same as mathematics. Multiplication and division happen before addition and subtraction, with the modulo operator being a sort of "funky division." Parentheses can be used to control this order.
None of this should be a surprise if you learned PEMDAS in school.
void main() {
// Following the order of operations:
// 2 * 3 + 3 * 9 / 2 - 2
// 2 * 3 happens first
// 6 + 3 * 9 / 2 - 2
// 3 * 9 happens next
// 6 + 27 / 2 - 2
// 27 / 2 happens next
// because of integer division, that gives 13
// 6 + 13 - 2
// 6 + 13 comes next
// 19 - 2
// and the final result is 17;
int result = 2 * 3 + 3 * 9 / 2 - 2;
System.out.println(result);
}
The ==
, !=
, >
, <
, >=
, and <=
operators play a part here too1. They all have a lower precedence order than all the math operators, so you can
put them in the middle of any two math expressions.
void main() {
// The == check happens last.
boolean areThingsSame = 3 * (4 - 1 + 3) * 4 == 5 * 3 + 1 * 3 * 9;
System.out.println(areThingsSame);
}
Every operator in the language has a defined order of operations with respect to all of the others. I am just showing them to you as they become relevant.
Reassignment
When the value of a variable is reassigned, the value stored in the variable before the reassignment can be used to compute the new value.
This is true for all data types, but it is easiest to demonstrate with numbers.
void main() {
int x = 1;
System.out.println(x);
// x starts as 1, 1 + 1 is 2.
// 2 is the new value of x.
x = x + 1;
System.out.println(x);
// x is now 2, 2 * 2 * 3 is 12
// 12 is the new value of x.
x = x * x * 3;
System.out.println(x);
}
This property was used in the previous example for the %
operator, but I think it worth calling attention to even if it is "intuitive".
Shorthands for Reassignment
A very common thing to do is to take the current value of a variable, perform some simple operation like addition on it, and reassign the newly computed value back into the variable.
void main() {
int x = 2;
System.out.println(x);
x = x * 5; // 10
System.out.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!
System.out.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;
System.out.println(x);
// Same as
// x = x + 1;
// x += 1;
x++;
System.out.println(x);
// Same as
// x = x - 1;
// x -= 1;
x--;
System.out.println(x);
}
Limits
Unlike in math, where numbers can be arbitrarily big or small, a Java int
is "fixed width."
Say you had a piece of paper that was only big enough to write two numbers on.
The only numbers you could write in a base ten system would be those from 0 to 99. You could not write 100 or anything larger.
A Java int
is similar except instead of only being able to write 0 to 99 on a piece of paper, a variable that has
the type int
can represent numbers from -231 to 231 - 1.
If you try to directly write out a number that is outside of that range, Java will not let you.
void main() {
// This will not run
int tooBig = 999999999999;
}
If you do math that should produce a larger number than is representable, the value will "loop around."
void main() {
// This is the value of 2^31 - 1
int atLimit = 2147483647;
// The value will "loop around" to -2^31
int beyondLimit = atLimit + 1;
// This will output -2147483648
System.out.println(beyondLimit);
}
There are other types which can represent a larger range of integers, as well as types
which do not have any limits, but for now int
is the only one you will need.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 5;
int y = 8;
System.out.println(x + y);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 5;
x--;
x--;
x = x + x;
System.out.println(x);
}
Challenge 3
Make it so that this program correctly determines if the numbers are even or not.
Assume that the values of x
, y
, and z
could be changed. Don't just write out
literally true
and false
for their current values.
void main() {
int x = 5;
int y = 4;
int z = 98;
boolean xIsEven = < CODE HERE >;
System.out.println(xIsEven);
boolean yIsEven = < CODE HERE >;
System.out.println(yIsEven);
boolean zIsEven = < CODE HERE >;
System.out.println(zIsEven);
}
Challenge 4
Try dividing a number by zero. What happens?
Write down your guess and then try running the program below to see.
void main() {
System.out.println(5 / 0);
}
Challenge 5
What can you write in the spot marked that will make the program output 2?
void main() {
int x = 5;
int y = <CODE HERE>;
System.out.println(x + y);
}
Challenge 6
What is the output of this code.1
void main() {
System.out.println(
6 / 2 * (1 + 2)
);
}
Floating Point Numbers
Floating point numbers are used to represent numbers
which cannot be stored as Integers like 2.5
or 3.14
.
double x = 1.5;
double y = 8.0;
double z = -3.14;
The type you will use to store a floating point number is double
.
double
stands for "double precision floating point."
Floating Point Literals
In order to write a floating point number in a program, you use a "floating-point literal."
1.5
Any number written with a decimal point is a floating point literal.
double pi = 3.14;
This includes numbers where a decimal point is written, but there is no fractional part to the number.
5.0
You cannot directly give a value to an integer variable using a floating point literal, even if there is no fractional part to the number.
// this will not work
int x = 5.0;
The reverse is possible though. You can give a value to a variable that stores a floating point number using an integer literal.
double x = 5;
Accuracy
Floating Point numbers are not an exact representation of numbers.
The reasons for this are twofold.
- It is much more efficient for a computer to work with data that is a "fixed size". You can't cram all the infinite possible numbers into 32, 64, or any fixed number of bits.
- For most systems, the inaccuracy is okay. When it is not, there are ways to do "real math" that we will cover much later.
For an explanation of the mechanics, I'll defer to this old Computerphile video.
Addition
You can add any two double
s using the +
operator.
void main() {
double x = 5.1;
// y will be 14.2
double y = x + 9.1;
System.out.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;
System.out.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;
System.out.println(x);
System.out.println(y);
System.out.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;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Subtraction
You can subtract any two double
s using the -
operator.
void main() {
double x = 5.1;
// y will be 4.1
double y = x - 9.2;
System.out.println(x);
System.out.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;
System.out.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;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Even if the result of such an expression will not have any fractional parts, you cannot directly assign it to an int
.
void main() {
int x = 5;
double y = 4;
// even though z would be 1, which can be stored in an int
// this will not work. The result of the expression is a double.
int z = x - y;
}
Multiplication
You can multiply any two double
s using the *
operator.
void main() {
double x = 3;
// y will be 27
double y = x * 9;
// z will be 13.5
double z = y * 0.5;
System.out.println(x);
System.out.println(y);
System.out.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;
System.out.println(a);
}
Division
You can divide any two double
s using the /
operator.
void main() {
double x = 8;
// y will be 4.0
double y = x / 2;
// z will be 1.3333333333333333
double z = y / 3;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Unlike with integer division, floating point division will include the remainder in the result.1
With the caveat that the result is now potentially inaccurate.
Equality
Just like int
s, double
s can be inspected to see if they are equal to one another using ==
.
void main() {
double numberOfToes = 10.0;
double numberOfFingers = 10.0;
boolean humanGenerated = numberOfToes == numberOfFingers;
System.out.println(humanGenerated);
}
Because of floating point inaccuracy, this might not always give you the result you expect though.
void main() {
double x = 0.1;
double y = 0.2;
// z will be 0.30000000000000004
double z = x + y;
// this will be false.
boolean doesWhatYouExpect = z == 0.3;
System.out.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;
System.out.println(fiveIsFive);
}
Comparison
In addition to comparing for equality with ==
and !=
, doubles
s can be compared to see if one is bigger than another using
>
, <
, >=
, and <=
.
This works the same as it does with int
s.
void main() {
double x = 1.5;
double y = 0.2;
// true
System.out.println(x > y);
// false
System.out.println(x < y);
}
Shorthands for Reassignment
All the same shorthands for reassignment work with double
s the same as they do with int
s.
void main() {
double x = 0.5;
// 0.5
System.out.println(x);
x += 3;
// 3.5
System.out.println(x);
x -= 1;
// 2.5
System.out.println(x);
x++;
// 3.5
System.out.println(x);
x--;
// 2.5
System.out.println(x);
x *= 5;
// 12.5
System.out.println(x);
x /= 2;
// 6.25
System.out.println(x);
}
NaN
There is a special floating point number called NaN
, which stands for "Not a Number."
You generally only encounter NaN
as the result of doing something silly like dividing zero by zero.
double nan = 0.0 / 0.0;
NaN
is not equal to itself.
void main() {
double nan = 0.0 / 0.0;
// will be false
boolean equalToItself = nan == nan;
System.out.println(equalToItself);
}
NaN
is not greater than itself.
void main() {
double nan = 0.0 / 0.0;
// will be false
boolean greaterThanItself = nan > nan;
System.out.println(greaterThanItself);
}
NaN
is not less than itself.
void main() {
double nan = 0.0 / 0.0;
// will be false
boolean lessThanItself = nan < nan;
System.out.println(lessThanItself);
}
NaN
is not greater than, less than, or equal to any number.
void main() {
double nan = 0.0 / 0.0;
// will all be false
System.out.println(nan < 5);
System.out.println(nan > 5);
System.out.println(nan == 5);
}
None of this is usually useful, but it is fun to know about.
Positive and Negative Infinity
In addition to the wackyness of NaN
, floating point numbers can also represent both positive
and negative infinity.
You can get positive infinity by dividing any positive number by zero.
double positiveInfinity = 1.0 / 0.0;
You can get negative infinity by dividing any negative number by zero.
double negativeInfinity = -1.0 / 0.0;
As you might expect, positive infinity is greater than any number and negative infinity is less than any number.
void main() {
double positiveInfinity = 1.0 / 0.0;
double negativeInfinity = -1.0 / 0.0;
// true
System.out.println(positiveInfinity > 99999999);
// true
System.out.println(negativeInfinity < -99999999);
}
Except for NaN
, of course.
void main() {
double positiveInfinity = 1.0 / 0.0;
double negativeInfinity = -1.0 / 0.0;
double nan = 0.0 / 0.0;
// false
System.out.println(positiveInfinity > nan);
// false
System.out.println(negativeInfinity < nan);
}
Square Root
A relatively common operation to want to perform on floating point numbers is to find their square root.
You can do this with Math.sqrt
.
void main() {
double x = 4;
double y = Math.sqrt(x);
// This will output 2
System.out.println(y);
}
You need to write Math.sqrt
and then inside of parentheses the expression whose value you want to take the square root of..
void main() {
double x = 5;
double y = 13;
double z = Math.sqrt(9 * x + y);
// This will output 7.615773105863909
System.out.println(z);
}
If you try to take the square root of a negative number, the result will be NaN
.
void main() {
// will output NaN
System.out.println(Math.sqrt(-5.2));
}
Conversion to Integers
Normally, a double
value cannot be assigned to an int
.
void main() {
double x = 5.0;
// will not work
int y = x;
}
The reason for this is that there are numbers like 2.5
, the infinities, and NaN
which do not have an
obvious way to be represented as an integer.
There are also numbers which a double
can represent like 4207483647.0
and -9999999999.0
which happen to be too big or too small to fit into the limits of an int
even though they
do have an obvious "integer representation."
As such, to make an int
out of a double
you need to accept that it is a "narrowing conversion."
The number you put in won't neccesarily be the number you get out.
To perform such a narrowing conversion, you need to put (int)
before a literal or expression that
evaluates to a double
.
void main() {
double x = 5.0;
// will be 5
int y = (int) x;
System.out.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;
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
Any number that is too big to store in an int
will be converted to the biggest possible int
, 231 - 1.
void main() {
// 2147483647
System.out.println((int) 4207483647.0);
double positiveInfinity = 5.0 / 0.0;
// 2147483647
System.out.println((int) positiveInfinity);
}
Any number that is too small to store in an int
will be converted to the smallest possible int
, -231.
void main() {
// -2147483648
System.out.println((int) -9999999999.0);
double negativeInfinity = -5.0 / 0.0;
// -2147483648
System.out.println((int) negativeInfinity);
}
And NaN
will be converted to zero.
void main() {
double nan = 0.0 / 0.0;
System.out.println((int) nan);
}
When you use (int)
to convert, we call that a "cast1 expression". The (int)
is a "cast operator." It even has
a place in the operator precedence order just like +
, -
, ==
, etc.
The main difference is that instead of appearing between two expressions like the +
in 2 + 5
, it appears to the left of a single expression.
https://english.stackexchange.com/questions/220001/etymology-of-type-cast
Conversion from Integers
To convert from an int
to a double
, you don't need to do any special work. All int
s are
representable as double
s so it is a "widening conversion" and will be handled automatically
by Java when performing an assignment.
void main() {
int x = 5;
double y = x;
System.out.println(x);
System.out.println(y);
}
This is not true in an expression. Even if the result of a computation between int
s is being assigned to
a double
, the computation will still be performed using the same rules int
s usually follow.
void main() {
int x = 7;
int y = 2;
// integer division of 7 and 2 gives 3.
double z = x / y;
System.out.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;
System.out.println(z);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
double x = 5.1;
double y = 2.4;
System.out.println(x + y);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
double x = 5.1;
double y = 2.1;
System.out.println(x + y);
}
Challenge 3
What will this program output when run? Write down your guess and then try running it.
How can you make it give the "right" answer?
void main() {
double x = 5 / 2;
System.out.println(x);
}
Challenge 4
These two expressions give different results. Why is that, and what results do they give?
void main() {
double resultOne = (int) 5.0 / 2 + 5.0 / 2;
double resultTwo = (int) (5.0 / 2 + 5.0 / 2);
System.out.println(resultOne);
System.out.println(resultTwo);
}
Challenge 5
The following is a quadratic equation.
\[ 2x^2 + 8x + 3 = 0 \]
To find the solutions of any quadratic equation you can use the following formula.
\[ x = \frac{-b \pm \sqrt{b^2 - 4ac} }{2a} \]
Where \(a\), \(b\), and \(c\) are the prefixes of each term in the following equation.
\[ ax^2 + bx + c = 0 \]
Write some code that finds both solutions to any quadratic equation defined by some variables
a
, b
, and c
. If the equation has imaginary solutions, you are allowed to just output NaN
.
void main() {
// For this one in particular, you should output
// -3.5811388300842 and -0.41886116991581
// but your code should work with these three numbers changed to
// represent any given quadratic equation.
double a = 2;
double b = 8;
double c = 3;
double resultOne = ???;
double resultTwo = ???;
System.out.println(resultOne);
System.out.println(resultTwo);
}
Characters
A character, represented by the data type char
, is a single
letter or symbol.
char letter = 'a';
I choose to pronounce it like the "char" in "Charmander."
Character Literals
In order to write a character in a program, you write that one character surrounded by single quotes.
'a'
This is called a "character literal." It has the same relationship to char
that an integer literal like 123 has to int
.
// Same as this "integer literal" is used to write a number
int sesameStreet = 123;
// A "character literal" is used to write text
char thisEpisodeIsBroughtToYouBy = 'c';
Common Escape Sequences
While most characters can be written directly into a program, as is the
case for a
, b
, or t
, there are some which cannot.
For these, you need to use what is called an "escape sequence."
The most common escape sequence you will use will be the one for a "new line."
Which is a backslash followed by an n
.
char newline = '\n';
Because a backslash is used for this special syntax, to put a backslash into a character literal you need to escape it with a backslash of its own.
char backslash = '\\';
And because single quotes are used to mark the start and end of a character literal, they need to be escaped as well.
char singleQuote = '\'';
Conversion to Integers
All char
s have a matching numeric value. 'a'
is 97
, 'b'
is 98
,
'&'
is 38
, and so on.
Same as assigning an int
to a double
, you can perform a widening conversion
by attempting to assign a char
to an int
.
void main() {
int valueOfA = 'a';
System.out.println(valueOfA);
}
char
s will be automatically converted to int
s when used with mathmatical operators like +
, -
, >
, <
, etc.
void main() {
char gee = 'g';
// all the letters from a to z have consecutive numeric values.
boolean isLetter = gee >= 'a' && gee <= 'z';
System.out.println(isLetter);
}
This can be useful if you are stranded on Mars1 or if you want to see if a character is in some range.
https://www.youtube.com/watch?v=k-GH3mbvUro
Conversion from Integers
An int
can represent more values than a char
, so conversion from int
to
char
requires the use of the cast operator (char)
.
void main() {
int x = 120;
char xAsChar = (char) x;
System.out.println(xAsChar);
}
This conversion is narrowing, so information might be lost if the int
value is too big or too small to fit into a char
.
The initial value of a char
can also be given by an integer literal if the integer literal represents a small enough letter.
void main() {
char z = 122;
System.out.println(z);
}
Unicode
Most letters and symbols that are common in the English speaking world fit into
a single char
, so pretending that a char
is always "a single
letter or symbol" is generally a good enough mental model.
Where this falls apart is with things like emoji (👨🍳) which are generally considered to be one symbol, but
cannot be represented in a single char
.
char chef = '👨🍳';
char
s are actually "utf-16 code units". Many symbols require multiple "code units" to represent.
For a full explanation, refer to this old Computerphile video.
It describes "utf-8", which is 8 bits per "code unit." Java's char
uses 16 bits, but that is the only difference.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
A lot of math problems ask you to find \( x^2 \). What is the value of the character x
squared?
Try to work it out on paper before running the program below.
void main() {
char x = 'x';
System.out.println(x * x);
}
Challenge 2
Alter the program below so that it will output true
if the character declared at the top is a letter.
Make use of the fact that the numeric values for a
- z
and A
- Z
are contiguous.
void main() {
char c = 'a';
boolean isLetter = ???;
System.out.println(isLetter);
}
Challenge 3
How many UTF-16 code units does it take to represent this emoji? 👨🍳
.
Strings
The String
data type is used to represent text.
String shortStory = "Everyone lived happily ever after, the end."
The word "string" comes from the fact that text is just individual characters "strung together".
As a concrete example j
, o
, and e
can be "strung together" into the "string"
joe
.
String Literals
In order to write text in a program, you surround it with double quotes.
"Hello, World"
This is called a "string literal." It has the same relationship to String
that an integer literal like 123
has to int
.
void main() {
// Same as this "integer literal" is used to write a number
int abc = 123;
// A "string literal" is used to write text
String name = "penelope";
}
Common Escape Sequences
Inside of a string literal, there are some characters that cannot be written normally.
An easy example is double quotes. You can't write double quotes in the middle of
a string literal because Java will think the extra quote is the end of the String
.
void main() {
String title = "The "Honorable" Judge Judy";
}
In order to make it work, the "
s need to be "escaped" with a backslash.
void main() {
String title = "The \"Honorable\" Judge Judy";
}
Since the backslash is used to escape characters, it too needs to escaped
in order to have it be in a String
. So to encode ¯\_(ツ)_/¯
into a String
you need to escape the first backslash.
void main() {
// The first backslash needs to be escaped. ¯\_(ツ)_/¯
String shruggie = "¯\\_(ツ)_/¯";
}
And much the same as with char
, you need to use \n
to write in a newline.
void main() {
String letter = "To Whom It May Concern,\n\nI am writing this letter to complain.";
}
The Empty String
There is a special String
which contains no characters at all.
void main() {
// There is nothing to say.
String conversationWithDog = "";
}
You write it just like any other string, just with nothing between the double quotes.
""
It is different from a String
that just contains spaces because to Java those "space characters"
are just as much real characters as a
, b
, or c
.
void main() {
// There is noteworthy silence.
String conversationWithInlaws = " ";
}
This is one of those things that feels totally useless, but comes in handy pretty often.
- Say you are writing a message to send to your friend. The messenger
app can represent the state of the input box before you type anything with
an empty
String
. - If you want to digitally record responses to legal paperwork, you might choose
to represent skipped fields as empty
String
s. - Video Games where characters have assigned names can assign an empty
String
as the name of otherwise "unnamed" characters. - etc.
Multiline Strings
If the text you want to store in a String
has multiple lines, you can use
three quotation marks to represent it in code.
void main() {
String poem = """
I met a traveller from an antique land,
Who said—“Two vast and trunkless legs of stone
Stand in the desert. . . . Near them, on the sand,
Half sunk a shattered visage lies, whose frown,
And wrinkled lip, and sneer of cold command,
Tell that its sculptor well those passions read
Which yet survive, stamped on these lifeless things,
The hand that mocked them, and the heart that fed;
And on the pedestal, these words appear:
My name is Ozymandias, King of Kings;
Look on my Works, ye Mighty, and despair!
Nothing beside remains. Round the decay
Of that colossal Wreck, boundless and bare
The lone and level sands stretch far away.
""";
}
Inside of the this "Multiline String Literal" you don't need to escape quotation marks "
and you gain the ability to write newlines without having to use \n
.
Concatenation
Any two strings can be concatenated by using the +
operator.
void main() {
String he = "he";
String llo = "llo";
String hello = he + llo;
System.out.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.";
System.out.println(message);
}
Equality
You can check if two String
s have the same contents by using .equals
.
void main() {
String lyricOne = "Green, Green, Dress";
String lyricTwo = "Green, Green, Dress";
boolean areSameLyric = lyricOne.equals(lyricTwo);
boolean isMyName = lyricOne.equals("Bop Bop");
System.out.println(areSameLyric);
System.out.println(isMyName);
}
You write one String
on the left, .equals
, and then the String
you want to check it
against inside of parentheses.
To see if strings have different contents, you need to use the not operator (!
) on
the result of .equals
.
void main() {
String bow = "bow";
String wow = "WOW";
boolean areNotSame = !bow.equals(wow);
System.out.println(areNotSame);
}
Length
The number of char
s which comprise a String
can be accessed by using .length()
.1
void main() {
String fruit = "strawberry";
int numberOfChars = fruit.length();
// strawberry is 10 characters long
System.out.println(
fruit + " is " + numberOfChars + " characters long"
);
}
This is different from the number of unicode codepoints.
Access Individual Characters
Given a String
, you can access the individual characters which
comprise it by using .charAt
.
The first character can be accessed by putting 0
in the parentheses.
The second by using 1
, and so on.
void main() {
String spy = "loid";
char l = spy.charAt(0);
System.out.println(l);
char o = spy.charAt(1);
System.out.println(o);
char i = spy.charAt(2);
System.out.println(i);
char d = spy.charAt(3);
System.out.println(d);
}
We call this number the "index" of the character.1
The index of the character to access can come from a variable.
void main() {
String assassin = "yor";
int indexOfR = 2;
char r = assassin.charAt(indexOfR);
System.out.println(r);
}
If you give a number equal to or greater than the length of the String
or a number less than zero,
you will get an error.
void main() {
String student = "anya";
// Crash!
student.charAt(999);
}
void main() {
String dog = "bond";
// Crash!
dog.charAt(-1);
}
There will be more things which have their individual elements accessible by indexes. They will all generally start from 0 for the first element but there are rare exceptions.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
String first = "1";
String second = "2";
String result = first + second;
System.out.println(result);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
String first = "1";
int second = 2;
System.out.println(first + second);
}
Challenge 3
What will this program output when run? Write down your guess and then try running it.
void main() {
char first = 'a';
String second = "b";
String third = "ab";
System.out.println((first + second).equals(third));
}
Challenge 4
Make it so this program will output abc
by only changing one line and
not altering the println
statement.
Before your change, why did it output 294
?
void main() {
char a = 'a';
char b = 'b';
char c = 'c';
System.out.println(a + b + c);
}
Challenge 5
Without adding any new println
s,
change one line in this program so that it outputs racecar
.
Try to find two ways to do that.
void main() {
String racecar = "racecar";
int diff = 1;
int index = 6;
System.out.print(racecar.charAt(index));
index += diff;
System.out.print(racecar.charAt(index));
index += diff;
System.out.print(racecar.charAt(index));
index += diff;
System.out.print(racecar.charAt(index));
index += diff;
System.out.print(racecar.charAt(index));
index += diff;
System.out.print(racecar.charAt(index));
index += diff;
System.out.println(racecar.charAt(index));
}
Branching Paths
All the code I have shown you so far has run from top to bottom. That is, it has followed a single "path."
Not all programs can follow a single path though.
Imagine trying to rent a car online. The first thing you might be asked is your age. This is because most car rental companies do not want to rent to people under the age of 25.1.
If you enter an age that is less than 25, the program should immediately tell you that you cannot rent a car. If you enter an age that is greater than or equal to 25, the program should continue to prompt you for more information.
There are multiple "branching paths" that the program might take.
For insurance reasons.
If
The way to represent a branching path in Java is by using an if
statement.
void main() {
int age = 5; // 👶
if (age < 25) {
System.out.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) {
System.out.println("You are too young to rent a car!");
}
}
If this condition evaluates to false
, then the code inside of {
and }
will not run.
Nested Ifs
The code inside of the {
and }
can be anything, including more if
statments.
void main() {
int age = 5; // 👶
if (age < 25) {
System.out.println("You are too young to rent a car!");
if (age == 24) {
System.out.println("(but it was close)");
}
}
}
When an if
is inside another if
we say that it is "nested".
If you find yourself nesting more than a few if
s that might be a sign that
you should reach out for help.
if (...) {
if (...) {
if (...) {
if (...) {
// Seek professional help
}
}
}
}
Else
When you want to do one thing when a condition evaluates to true
and another when that same condition evaluates to false
you can use else
.
void main() {
int age = 30; // 🙎♀️
if (age < 25) {
System.out.println("You cannot rent a car!");
}
else {
System.out.println("You might be able to rent a car.");
}
}
You write the word else
immediately after the }
at the end of an if
statement, then
some code inside of a new {
and }
.
if (CONDITION) {
<CODE TO RUN>
}
else {
<CODE TO RUN>
}
When the condition evaluates to false
, the code inside of else
's {
and }
will run.
else
cannot exist without a matching if
, so this code does not work.
void main() {
else {
System.out.println("No if.");
}
}
Else If
If you have an if
nested in an else
branch, you can simplify that by using
else if
.
void main() {
boolean cool = true; // 🕶️
int age = 30; // 🙎♀️
if (age < 25) {
System.out.println("You cannot rent a car!");
}
else {
if (!cool) {
System.out.println("You failed the vibe check.");
}
else {
System.out.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) {
System.out.println("You cannot rent a car!");
}
else if (!cool) {
System.out.println("You failed the vibe check.");
}
else {
System.out.println("You are rad enough to rent a car.");
}
}
You can have as many else if
s as you need. Each one will only run if all the previous conditions
evaluate to false
.
void main() {
boolean cool = true; // 🕶️
int age = 100; // 👴
if (age < 25) {
System.out.println("You cannot rent a car!");
}
else if (!cool) {
System.out.println("You failed the vibe check.");
}
else if (age > 99) {
System.out.println("You are too old to safely drive a car!");
}
else if (age > 450) {
System.out.println("There can only be one! ⚔️🏴");
}
else {
System.out.println("You are rad enough to rent a car.");
}
}
Relation to Delayed Assignment
Delayed assignment of variables becomes useful with if
and else
.
So long as Java can figure out that a variable will always be given an initial value
inside of an if
and else
, you will be allowed to use that variable.
void main() {
int age = 22;
String message;
if (age > 25) {
message = "You might be able to rent a car";
}
else {
message = "You cannot rent a car!";
}
System.out.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.
System.out.println(message);
}
Scoped Variables
If you make a variable declaration inside of an if
or an else
block,
that declaration will be "scoped" to the block.
void main() {
int age = 5;
if (age == 5) {
int nextAge = age + 1;
System.out.println(nextAge);
}
// If you uncomment this line, there will be an issue
// `nextAge` is not available to the scope outside of the `if`
// System.out.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"
System.out.println(message);
}
This is why you will sometimes need to use delayed assignment.
Conditional Operator
When the only operation being performed inside of an if
and else
pair
is setting the initial value of a variable, you can use the "conditional operator"1
to perform that assignment instead.
void main() {
int age = 22;
String message = age < 25
? "You cannot rent a car!"
: "You might be able to rent a car";
System.out.println(message);
}
You write a condition followed by a ?
, a value to use when that condition evaluates to true
, a :
,
and then a value to use when that condition evaluates to false
.
CONDITION ? WHEN_TRUE : WHEN_FALSE
Some people will call this a ternary expression. Ternary meaning "three things." Same idea as tres leches.
Boolean Expressions
A common thing I've seen students do is set the initial value of some
boolean
variable based on some condition.
void main() {
int age = 22;
boolean canRent;
if (age > 25) {
canRent = true;
}
else {
canRent = false;
}
// or
// boolean canRent = age > 25 ? true : false;
System.out.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;
System.out.println(canRent);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write code that will outputs The number is even
if x
is an even number.
void main() {
// Change this variable to different numbers
// to test your code
int x = 5;
// < YOUR CODE HERE >
}
Challenge 2
Make it so that your code from the previous problem will also output The number is odd
if the number is odd.
Challenge 3
Write code that will output allowed
if the the password
variable is equal to
"abc123"
and not allowed
if it isn't.
void main() {
// Change this variable to different strings
// to test your code
String password = "apple";
// < YOUR CODE HERE >
}
Challenge 4
Write code that will assign the string The number {x} is even
to message
if x
is an even number
and The number {x} is odd
if x
is an odd number.
So if x
is 12 the string you should assign The number 12 is even
to message
.
void main() {
String message;
// Change this variable to different numbers
// to test your code
int x = 5;
// < YOUR CODE HERE >
System.out.println(message);
}
Loops
if
and else
let you write programs which can take branching paths,
but they will still run from the beginning to the end.
Not all programs can just end though. Video games should draw their world at one second and do it again the next. If you enter the wrong password on your phone, it should ask you for your password again.
This is what "loops" are for. You run code starting from some point and then loop back to that same point and run that code again.
While
One way to make a loop in code is to use while
.
void main() {
int x = 5;
while (x != 0) {
System.out.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) {
System.out.println(
glassesOfMilk + " glasses of milk left"
);
glassesOfMilk--;
}
}
If a loop is made with while
we call it a "while loop."1
"We called him Tortoise because he taught us." - Lewis Carroll
Endless Loops
If a while loop will never end, we call that an endless loop.
This can happen if the condition is a constant like while (true)
void main() {
while (true) {
System.out.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) {
System.out.println("It goes on and on my friends");
}
}
Many games should never really "finish" so at the very start of that sort of program it is not uncommon
to see a while (true)
.
Break
While loops will usually stop running when the condition at the top evaluates
to false
.
This can be bypassed by using the break
statement.
void main() {
int x = 5;
while (x > 0) {
if (x == 2) {
break;
}
x--;
}
System.out.println(
"Final value of x is " + x
);
}
If a break
is reached, the code in the loop stops running immediately.
The condition of the loop is not checked again.
This can be useful in a variety of situations, but notably it is the only way to exit from an otherwise endless loop.
void main() {
while (true) {
System.out.println(
"The people started singing it not knowing what it was"
);
// Will immediately leave the loop
break;
}
}
Continue
Unless there is a break
, while loops will usually run all the code in their body from top to bottom.
The only other situation this will not happen is if a continue
statement is reached.
void main() {
// Will output a message for every number except 4
int x = 5;
while (x > 0) {
if (x == 4) {
continue;
}
System.out.println(x + " is a good number");
x--;
}
}
If a continue
is reached the code in the loop stops running immediately but, unlike break
,
the condition of the loop is checked again. If it still evaluates to true
then the code
in the loop will run again.
Unreachable Code
If you write some code directly after a break
or continue
that code will be "unreachable."
Java knows this and so won't let any code like that run.
void main() {
// This will not work
while (true) {
continue;
System.out.println("this is unreachable");
}
}
Do
One variation on a while
loop is a "do-while loop."
void main() {
int x = 0;
do {
System.out.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 {
System.out.println("this will run");
} while (x != 0);
while (x != 0) {
System.out.println("this will not run");
}
}
One way to remember the difference is that in a "do-while loop" you always "do the thing" at least once.
Nested Loops
Just like with if
, The code inside of the {
and }
can be anything, including more loops.
void main() {
int x = 5;
int y = 3;
while (x != 0) {
while (y != 0) {
System.out.println(
"x is " + x
);
System.out.println(
"y is " + y
);
x--;
y--;
}
}
}
If you are inside such a "nested loop", continue
and break
apply to the
"closest" loop.
That is, if a continue
or a break
were to appear here
void main() {
while (x != 0) {
while (y != 0) {
if (y == 2) {
break;
}
System.out.println(
"x is " + x
);
System.out.println(
"y is " + y
);
x--;
y--;
}
}
}
Then the y != 0
loop will be broken out of, not the x != 0
one.
And if a continue
or a break
were to appear here
void main() {
while (x != 0) {
if (x == 2) {
break;
}
while (y != 0) {
System.out.println(
"x is " + x
);
System.out.println(
"y is " + y
);
x--;
y--;
}
}
}
Then the x != 0
loop would be the "target."
Labeled Break
If you want to break out of a nested loop from one of the inner loops, you can use a "labeled break."
void main() {
outerLoop:
while (true) {
while (true) {
break outerLoop;
}
}
}
To do this, before your outer while or do-while loop you need to add a "label" followed by a :
.
A label is an arbitrary name just like a variable's name.
<LABEL>:
while (<CONDITION>) {
<CODE HERE>
}
<LABEL>:
do {
<CODE HERE>
} while (<CONDITION>);
Then inside of an inner loop, you just need to write break
followed by the label name.
void main() {
int x = 5;
int y = 3;
xLoop:
while (x != 0) {
while (y != 0) {
if (x == 2 && y == 2) {
break xLoop;
}
System.out.println(
"x is " + x
);
System.out.println(
"y is " + y
);
x--;
y--;
}
}
System.out.println("done.");
}
In this case the x != 0
loop will be broken out of, not the y != 0
one.
Labeled Continue
In the same way a labeled break can break out of a nested loop, a labeled continue can jump back to the start of a nested loop.
You just write continue
followed by the label name.
void main() {
// Will keep going back to the top of the outer loop
outerLoop:
while (true) {
System.out.println("inside outer loop");
while (true) {
System.out.println("inside inner loop");
continue outerLoop;
}
}
}
Iteration
Loops potentially run code multiple times. Each time one goes from its top to its bottom we call that an "iteration" of the loop.
void main() {
int x = 0;
while (x < 5) {
// On the 1st iteration x will be 0
// On the 2nd iteration x will be 1
// ...
// On the final iteration x will be 4
System.out.println(x);
x++
}
}
When the purpose of a loop is to run for every thing in some sequence of things, we say that the loop is "iterating over" those things.
Counting Up
Say your loop is intended to run some code for every number from 1
to 100
.
The general pattern for code like this is to have some variable which tracks the current number, a loop whose condition is that the number is less than the number you want to stop at, and a line at the bottom of the loop which increments the current number.
void main() {
int currentNumber = 1;
while (currentNumber <= 100) {
System.out.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) {
System.out.println(currentNumber);
currentNumber++;
}
}
Counting Down
If you want to do the opposite, starting from a number like 100
and count down to 1
,
the pattern will be similar.
You still have some variable tracking the current number, but with a loop whose condition is that the number is greater than the number you want to stop at, and a line at the bottom of the loop which decrements the current number.
void main() {
int currentNumber = 100;
while (currentNumber >= 1) {
System.out.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) {
System.out.println(currentNumber);
currentNumber--;
}
}
Iterate over a String
This general pattern of counting up and counting down becomes
especially useful when you want to iterate over each character in
a String
.
void main() {
String name = "Avril";
int index = 0;
while (index < name.length()) {
System.out.println(name.charAt(index));
index++;
}
}
Challenges
Early on, most students tend to have a lot of trouble with loops. Its also what is quizzed on in a lot of standardized tests.
Because of that there will be a lot of challenges in this section for you to practice. Try to at least do the first ten or so to make sure you have the concept down, but the more the better.
It might take awhile before you feel truly comfortable with this. That is normal.
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write code that outputs every number from 1
to 10
.
void main() {
<CODE HERE>
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x < 10) {
System.out.println(x);
x++;
}
}
Challenge 3
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x <= 10) {
System.out.println(x);
x++;
}
}
Challenge 4
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x < 10) {
if (x % 3 == 0) {
break;
}
System.out.println(x);
x++;
}
}
Challenge 5
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x < 10) {
if (x % 3 == 0) {
continue;
}
System.out.println(x);
x++;
}
}
Challenge 6
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 1;
while (x < 10) {
int y = 2;
while (y < 5) {
System.out.println(x * y);
y++;
}
x++;
}
}
Challenge 7
Write code that will output each character of name
on its own line.
So for if name
is equal to "Bridget"
, I would expect the following as output.
B
r
i
d
g
e
t
void main() {
<CODE HERE>
}
Challenge 8
Write code that will output each character of name
on its own line, starting with the last
character and going backwards.
So for if name
is equal to "Samantha"
, I would expect the following as output.
a
h
t
n
a
m
a
S
void main() {
// Change this value to test your code.
String name = "Samantha";
// <CODE HERE>
}
Challenge 9
Write code that will take a number and if it is divisible by two, divides it by two. If it is not, multiplies it by three and adds one.
Keep doing this until the number equals one. Output it each time.
If the initial number is 6
you should have this as output.
6
3
10
5
16
8
4
2
1
If the initial number is 15
you should have this as output.
15
46
23
70
35
106
53
160
80
40
20
10
5
16
4
2
1
void main() {
// Change this value to test your code.
int n = 15;
// <CODE HERE>
}
Challenge 10
Write code that outputs every third number from 37
to 160
.
void main() {
<CODE HERE>
}
Challenge 11
Write code that outputs the number of vowels in name
. Treat y
as a vowel.
Treat the characters a
, A
, e
, E
, i
, I
, o
, O
, u
, U
, y
, and Y
as vowels.
void main() {
// Change this value to test your code.
String name = "Damian";
// <CODE HERE>
}
Challenge 12
Write code that outputs {name} is mostly vowels
if the number of vowels in name
is greater
than the number of consonants. and {name} is mostly consonants
if the opposite is true.
Output {name} has an equal number of vowels and consonants
if the count of both is the
same.
Make sure to not treat non-alphabetic characters like !
and ?
as consonants.
void main() {
// Change this value to test your code.
String name = "Messi";
// <CODE HERE>
}
Challenge 13
Rewrite the following code to not have the shouldBreak
variable
and instead to use a labeled break.
void main() {
// Don't think too hard about what these numbers mean.
int x = 3;
int y = 0;
boolean shouldBreak = false;
while (!shouldBreak && x < 100) {
while (y < 100) {
System.out.println("x is " + x);
System.out.println("y is " + y);
x = x * y;
if (x == 0) {
shouldBreak = true;
break;
}
y++;
}
}
System.out.println("Done");
}
Arrays
Arrays are used to represent a fixed-size collection of things.
int[] oddNumbers = { 1, 3, 5, 7, 9 };
Fixed-size means that once an array is made, it will always hold the same number of things.
We call the things stored in an array its "elements."
You can make an array of any type of element by using the name of the type followed by
[]
.
char[] letters = { 'a', 'b', 'c' };
String[] words = { "its", "as", "easy", "as" }
int[] numbers = { 1, 2, 3 };
double[] doubles = { 97.0, 98.0, 99.0, 1.0, 2.0, 3.0 };
Array Initializers
To give an initial value to an array you can use an array initializer.
After the equals sign you write {
followed by a comma separated list of elements and a final }
.
int[] numbers = { 1, 2, 3 };
// |---------|
// this part is
// the initializer
The elements in an initializer do not have to be literals and can also be variables or expressions.
int two = 2;
// Will hold 1, 2, 3 just like the array above
int[] numbers = { 1, two, two + 1 }
We call them array initializers because you use them to give an initial value to an array.1
You may be noticing a pattern. Confusing sounding names are often kinda "obvious" with enough context.
Length
The number of elements which comprise an array can be accessed by using .length
.1
void main() {
String[] veggies = { "brussels", "cabbage", "carrots" };
int numberOfElements = veggies.length;
// veggies is 3 elements long
System.out.println(
"veggies is " + numberOfElements + " characters long"
);
}
Unlike with a String
, you do not write ()
after .length
.
Access Individual Elements
Given an array, you can access any of its elements by index.
You write the name of a variable containing an array, [
, a number, and then a ]
.
The first element can be accessed by using 0
, the second by using 1
, and so on.
void main(){
String[] lyrics = { "you", "say", "goodbye" };
String you = lyrics[0];
System.out.println(you);
String say = lyrics[1];
System.out.println(say);
String goodbye = lyrics[2];
System.out.println(goodbye);
}
The index of the element can also come from a variable.
void main(){
int index = 2;
String[] lyrics = { "I", "say", "hello" };
System.out.println(lyrics[index]);
}
If you give a number equal to or greater than the length of the array or a number less than zero, you will get an error.
void main(){
String[] lyrics = { "I", "say", "hi" };
// Crash!
System.out.println(lyrics[999]);
}
void main(){
String[] lyrics = { "you", "say", "low" };
// Crash!
System.out.println(lyrics[-1]);
}
Set Individual Elements
You can also set any of the elements of an array to have a new value.
To do this, on the left hand side of an equals sign you write the name of a variable
followed by [
, an index, and ]
. Then on the right hand side of the equals you write
the new value.1
void main() {
String[] sentence = { "you", "are", "found", "guilty" };
System.out.println(
sentence[0]
+ " "
+ sentence[1]
+ " "
+ sentence[2]
+ " "
+ sentence[3]
);
sentence[1] = "aren't";
System.out.println(
sentence[0]
+ " "
+ sentence[1]
+ " "
+ sentence[2]
+ " "
+ sentence[3]
);
}
The index of the element to set can also come from a variable.
void main() {
int index = 2;
String[] response = { "and", "it", "isn't", "opposite", "day" };
System.out.println(
response[0]
+ " "
+ response[1]
+ " "
+ response[2]
+ " "
+ response[3]
+ " "
+ response[4]
);
response[2] = "is";
System.out.println(
response[0]
+ " "
+ response[1]
+ " "
+ response[2]
+ " "
+ response[3]
+ " "
+ response[4]
);
}
If you give a number equal to or greater than the length of the array or a number less than zero, you will get an error.
void main() {
String[] response = { "objection" };
// Crash
response[1] = "!";
}
void main() {
String[] response = { "sustained" };
// Crash
response[-1] = "not";
}
You cannot change the contents of a String
like you would an array. This is one of the biggest differences between a String
and a char[]
.
Aliasing
When you assign a variable containing an array to another variable, the array referenced by both variables will be the exact same.
This means that if the contents of the array are updated, that change will be reflected by both variables.
void main() {
char[] lettersOne = { 'B', 'a', 't', 'm', 'a', 'n' };
char[] lettersTwo = lettersOne;
// Batman
System.out.println(lettersOne);
// Batman
System.out.println(lettersTwo);
lettersOne[0] = 'C';
// Catman
System.out.println(lettersOne);
// Catman
System.out.println(lettersTwo);
lettersTwo[0] = 'R';
// Ratman
System.out.println(lettersOne);
// Ratman
System.out.println(lettersTwo);
}
When two variables point to the same thing like this we say that both variables are "aliases" for eachother.
Reassignment
The length of an array cannot change, but a variable holding an array can be reassigned to a new array that has a different length.
When reassigning the value of an array variable you need to put new
followed by a space, the type
of element held by the array, and then []
all before the initializer.
So to reassign an int[]
you need to write something like new int[] { 1, 2, 3 }
.
void main() {
int[] numbers = { 1, 2 };
// 2
System.out.println(numbers.length);
numbers = new int[] { numbers[0], numbers[1], 3 };
// 3
System.out.println(numbers.length);
}
This reassignment will not be affect any variables which are aliases for the variable's old value.
void main() {
char[] wordOne = { 'g', 'o' };
char[] wordTwo = wordOne;
// go
System.out.println(wordOne);
// go
System.out.println(wordTwo);
wordOne = new char[] { wordOne[0], wordOne[1], 's', 'h' };
// gosh
System.out.println(wordOne);
// go
System.out.println(wordTwo);
wordTwo[0] = 'n';
// gosh
System.out.println(wordOne);
// no
System.out.println(wordTwo);
wordOne[0] = 'p';
// posh
System.out.println(wordOne);
// no
System.out.println(wordTwo);
}
Relation to Final Variables
Just like anything else, arrays can be stored in variables marked final
.
This means that the variable cannot be reassigned, but it does not mean that the array's contents cannot be changed directly or through an alias.
void main() {
final char[] catchphrase = { 'w', 'o', 'a', 'h', '!' };
// woah!
System.out.println(catchphrase);
// Cannot reassign
// catchphrase = { 'e', 'g', 'a', 'd', 's' }
// but can set elements directly
catchphrase[0] = 'e';
catchphrase[1] = 'g';
// or through an alias
char[] alias = catchphrase;
alias[2] = 'a';
alias[3] = 'd';
alias[4] = 's';
// egads
System.out.println(catchphrase);
}
Printing the Contents of an Array
If you try to use System.out.println
to output a String[]
you won't see the contents of the array. Instead you will see
something like [Ljava.lang.String;@1c655221
.
void main() {
String[] shout = { "fus", "ro", "dah" };
// [Ljava.lang.String;@5a07e868
System.out.println(shout);
}
A similar thing will happen with int[]
, boolean[]
, and double[]
.1
void main() {
int[] nums = { 11, 11, 11 };
// [I@5a07e868
System.out.println(nums);
boolean[] bools = { true, false };
// [Z@5a07e868
System.out.println(bools);
double[] doubles = { 1.1, 1.1, 1.1 };
// [D@5a07e868
System.out.println(bools);
}
The only kind of array which will include its contents when printed is a char[]
.
It will be printed as if it were a String
.
void main() {
char[] continent = { 'T', 'a', 'm', 'r', 'i', 'e', 'l' };
// Tamriel
System.out.println(continent);
}
If you want to actually see the contents of an array, you should use a loop.2
void main() {
String[] factions = { "empire", "stormcloaks", "dragons" };
int index = 0;
while (index < factions.length) {
System.out.println(factions[index]);
index++;
}
}
What [I@5a07e868
and co. mean isn't really important. Try not to get too distracted by it.
Later on, there will be easier ways to do this sort of inspection. This is just the one I can demonstrate now.
Empty Array
If you use an array initializer that has no elements between the {
and }
you can create an empty array.
char[] emptyCharArray = {};
An empty array is very similar to an empty String
. It has a length of 0, it has no elements,
and it is generally useful only as a placeholder value for when you have no data yet but will
be able to reassign the variable holding it when you get some.
void main() {
char[] emptyCharArray = {};
// 0
System.out.println(emptyCharArray.length);
// Crash
System.out.println(emptyCharArray[0]);
}
Difference between Initializer and Literal
The reason { 1, 2, 3 }
is called an "array initializer" and not
an "array literal" is somewhat subtle.
When you have a literal, like a String
literal, you can assign that to a variable and
then use that String
afterwards.
void main() {
String name = "Alana";
// l
System.out.println(name.charAt(1));
}
But you can also perform those operations using the literal itself, without an intermediate variable.
void main() {
// l
System.out.println("Alana".charAt(1));
}
Array initializers work in the case where you first assign them to a variable before using the array.
void main() {
char[] name = { 'A', 'm', 'a', 'n', 'd', 'a' };
// m
System.out.println(name[1]);
}
But they do not work to perform operations on directly.
void main() {
// Will not run
System.out.println({ 'A', 'm', 'a', 'n', 'd', 'a' }[1]);
}
Initialization with new
Before the initializer for an array, you are allowed to write
new
followed by a space, the type of thing in the array,
and an empty []
.
void main() {
char[] mainCharacter = { 'A', 'a', 'n', 'g' };
System.out.println(mainCharacter);
char[] sideCharacter = new char[] { 'A', 'a', 'n', 'g' };
System.out.println(sideCharacter);
}
This is required for performing delayed initialization of a variable holding an array.
void main() {
char[] element;
element = new char[] { 'f', 'i', 'r', 'e' };
System.out.println(element);
// This would not work
// element = { 'f', 'i', 'r', 'e' };
}
One ability this gives you is to use an array in an expression. I.E.
the initializer coupled with the new char[]
is akin to an "array expression."
void main() {
System.out.println(new char[]{ 'K', 'a', 't', 'a', 'r', 'a' }[1]);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Edit the following program so that the output is zero.
void main() {
// Only change this line
String[] words = { "Sam", "I", "Am" };
System.out.println(array.length);
}
Challenge 2
Using only System.out.println
and array accesses,
print hello world
to the screen.
void main() {
char[] letters = {
' ',
'h',
'e',
'l',
'o',
'w',
'r',
'd'
};
// Your code here
}
Challenge 3
Without editing either the array declaration or the loop at the bottom,
make the output of this program 0
.
void main() {
final int[] numbers = { 1, 2, 3, 4 };
// -----------
// YOUR CODE HERE
// -----------
int total = 0;
int index = 0;
while (index < numbers.length) {
total += numbers[index];
index += 1;
}
System.out.println(total);
}
Challenge 4
Make this program output bulbasaur
without changing anything
above or below the marked areas.
void main() {
char[] name = { 'b', 'u', 'l', 'b' };
// -----------
// YOUR CODE HERE
// -----------
char[] toPrint = name;
System.out.println(toPrint);
}
Loops II
while
loops are enough to make any looping logic that you might want, but
they aren't the only kind of loops you will see.
There are tasks which would require a while
loop, but are common enough that there are other kinds
of loops that are shortcuts to writing them.
For
The for
loop is a shortcut to writing a while
loop which
has distinct steps that
- Declare a variable
- Check that variable to know whether to stop iterating
- Update the variable at the end of each iteration
As with many things, this might be easiest to see by looking at an example.
void main() {
// Will run 10 times
for (int number = 0; number < 10; number++) {
System.out.println(number);
}
}
That for
loop works about the same as this while
loop.
void main() {
int number = 0;
while (number < 10) {
System.out.println(number);
number++;
}
}
For Syntax
A for
loop has three distinct parts.
-
An initializer.
-
An expression which evaluates to a
boolean
. -
A statement.
for (<INITIALIZER> ; <EXPRESSION> ; <STATEMENT>) {
<CODE HERE>
}
The initializer is a statement which declares and initializes a variable
like int number = 0
.
The expression is some check like number < 5
that is checked each iteration
to know if the loop should continue.
The statement is ran at the end of every iteration, generally updating the variable tracked by the initializer and expression.
These can be thought of as being the same as a while
loop written like so.
<INITIALIZER>;
while (<EXPRESSION>) {
<CODE HERE>
<STATEMENT>;
}
Counting Up and Down
One of the easiest things to do with a for
loop is count up to or down from
a given number.
void main() {
// Goes from 1 to 100
for (int currentNumber = 1; currentNumber <= 100; currentNumber++) {
System.out.println(currentNumber);
}
// Goes from 100 to 1
for (int currentNumber = 100; currentNumber >= 1; currentNumber--) {
System.out.println(currentNumber);
}
}
You use the initializer to set some variable to a starting number like int currentNumber = 1
,
have the expression check if the number is at the target end number like currentNumber <= 100
,
and use the statement to change the number by one like currentNumber++
.1
Very often, if you are given a test on for
loops it will focus on doing all sorts of counting up, down, and around. Be prepared.
Iterate over a String
As was shown with while
loops, being able to count up and down lets
you iterate over each character in a String
.
void main() {
String name = "Lavigne";
for (int index = 0; index < name.length(); index++) {
System.out.println(name.charAt(index));
}
}
Iterate over an Array
In the same way you can use a for
loop to go through each character of a String
,
you can use it to go through each element in an array.
void main() {
int[] numbers = { 4, 1, 6, 9 };
for (int index = 0; index < numbers.length; index++) {
System.out.println(numbers[index]);
}
}
The only difference from String
s is that instead of .length()
and .charAt(...)
, you use .length
and []
.
Comparison to while
If you were to compare the code needed to loop over an array using a for
loop
and the code needed with a while
loop, there might not seem like much of a difference.
void main() {
double[] numbers = { 4.4, 1.1, 4.1, 4.7 };
for (int index = 0; index < numbers.length; index++) {
System.out.println(numbers[index]);
}
int index = 0;
while (index < numbers.length) {
System.out.println(numbers[index]);
index++;
}
}
This is doubly true when we are looking at toy examples where the only thing done
with the element is System.out.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 eachother are separated by long distances.
In this dimension, for loops are superior. All the bits of code that "control the loop" can be right at the top.
void main() {
double[] numbers = { 4.4, 1.1, 4.1, 4.7 };
for (int index = 0; index < numbers.length; index++) {
/*
Can
potentially
have
arbitrary
code
you want to run
a bunch
of times
*/
}
}
i
One thing you will very often see if you read other peoples'
code is that the variable being tracked in a for loop is often called
i
.
void main() {
String word = "bird";
for (int i = 0; i < array.length; i++) {
char letter = word.charAt(i);
System.out.println(letter);
}
// b
// i
// r
// d
}
While usually naming variables with single letters isn't a great idea,
most developers carve out an exception for cases like this. Writing index
gets
tedious.
Its also helpful to go i -> j -> k
when you end up nesting for
loops.1
void main() {
char[] letters = { 'A', 'B', 'C' };
int[] numbers = { 1, 2 };
for (int i = 0; i < letters.length; i++) {
for (int j = 0; j < numbers.length; j++) {
System.out.print(letters[i]);
System.out.println(numbers[j]);
}
}
// A1
// A2
// B1
// B2
// C1
// C2
}
Just do not start naming all your variables single letters.
j
and k
standing for jindex an kindex respectfully.
Break
break
works the same with for
loops as it does with while
loops.
Any time you hit a line with break
you will immediately exit the loop.
void main() {
for (int i = 0; i < 1000; i++) {
if (i == 5) {
break;
}
System.out.println(i);
}
System.out.println("Over");
// 0
// 1
// 2
// 3
// 4
// Over
}
Continue
continue
works slightly differently with for
loops than how it does with while
loops.
Any time you hit a line with continue
you will immediately jump back to the top of the loop, but
unlike with a while
loop, the statement which updates your variable will still run.
void main() {
for (int i = 0; i < 5; i++) {
if (i == 2) {
// i++ will still run
continue;
}
System.out.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;
}
System.out.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;
}
System.out.println(i);
i++;
}
// 0
// 1
// 3
// 4
}
Delayed Assignment
The initializer of a for
loop can give an initial value to a variable
declared outside of the loop.
void main() {
int number;
for (number = 0; number < 5; number++) {
System.out.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++) {
System.out.println("At: " + number);
}
// This will work, we can access the variable still.
System.out.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++) {
System.out.println("At: " + number);
}
// This will not work. number is no longer available
System.out.println("Ended at: " + number);
}
Inferred Types
The initializer of a for
loop works the same as any variable assignment, so
you still are allowed to use var
so that the type of the declared variable is inferred.
void main() {
for (var i = 0; i < 10; i++) {
System.out.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") {
System.out.println(repeated);
}
// a
// aa
// aaa
// aaaa
}
Empty Initializers
You are allowed to leave the initializer part of a for
loop blank
so long as you still have the ;
.
void main() {
int number = 0;
for (;number < 5; number++) {
System.out.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++) {
System.out.println(number);
}
System.out.println("Still have number: " + number);
}
Empty Expressions
You are also allowed to leave the expression part of a for
loop blank.
void main() {
for (int i = 0;;i++) {
System.out.println(i);
}
// 0
// 1
// 2
// 3
// ... and so on
}
This means that each time through there is no check to see if the loop will exit.
The loop will only exit if there is an explicit break
somewhere.
void main() {
for (int i = 0;;i++) {
if (i == 5) {
break;
}
System.out.println(i);
}
// 0
// 1
// 2
// 3
// 4
}
Empty Statements
You can even leave the statement part of a for
loop blank. This means that at
the end of an iteration there is nothing guaranteed to run.
void main() {
for (int i = 6; i > 2;) {
System.out.println(i);
i--;
}
// 6
// 5
// 4
// 3
// 2
}
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;) {
System.out.println(number);
number *= 2;
}
// Same logic as above
int number2 = 1;
while (number2 < 10) {
System.out.println(number2);
number2 *= 2;
}
}
If you leave the initializer, expression, and statement blank it will be the same as a while (true)
loop.
for (;;) {
System.out.println("The people stated singing it...");
}
// Runs forever
The only difference is that (;;)
looks somewhat like
- A spider
- The Pokémon Kabuto
- A person crying
And that can be fun.
Final Variables
The initializer of a for
loop can also declare final
variables.
void main() {
int i = 0;
for (final String name = "Bob"; i < 5; i++) {
System.out.println(name + ": " + i);
}
}
This doesn't have much use with loops that track int
s and String
s, but if you
are feeling clever you can use this ability along with arrays or other things you
can change without reassigning a variable.
void main() {
for (final char[] letters = { 'I', 'O', 'U' }; letters[0] != 'A';) {
for (int i = 0; i < letters.length; i++) {
letters[i] -= 1;
System.out.print(letters[i]);
}
System.out.println();
}
// HNT
// GMS
// FLR
// EKQ
// DJP
// CIO
// BHN
// AGM
}
There aren't many reasons to do this, but it is in fact not against the law and I cannot stop you.
Labeled Break
Labeled breaks work the same with for
loops as they do with while
loops.
outerLoop:
for (;;) {
for (;;) {
break outerLoop;
}
}
This applies also to when while
loops are nested within for
loops or the other way around.
void main() {
outerForLoop:
for (int i = 0; i < 10; i++) {
System.out.println(i);
while (i < 100) {
if (i == 5) {
break outerForLoop;
}
i++;
}
System.out.println(i);
}
// 0
}
Labeled Continue
Labeled continues also work the same in for
loops as while
loops, but with the hopefully expected caveat that
the statement of a for
loop will always run when you get to the top of it.1
void main() {
label:
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
System.out.println ("" + i + ", " + j);
if (i == 2) {
// i++ will run
continue label;
}
}
}
// 0, 0
// 0, 1
// 0, 2
// 1, 0
// 1, 1
// 1, 2
// 2, 0
// 3, 0
// 3, 1
// 3, 2
}
Drawing Right Triangles
One of the more fun things to do with for
loops1 is to use them to print out shapes.
Say you wanted to draw this right triangle.
*
**
***
There is one *
, then two *
s on the next line, and three *
s on the last.
If you were to write the code out to print this explicitly it would look like this.
void main() {
System.out.print("*\n**\n**\n");
}
Where \n
is explicitly putting in the new lines.
Since counting up 1 -> 2 -> 3
is easy with for
loops, you can translate this
void main() {
for (int numberOfStars = 1; numberOfStars <= 3; numberOfStars++) {
for (int i = 0; i < numberOfStars; i++) {
System.out.print("*");
}
// Same as System.out.print("\n");
System.out.println();
}
}
Which makes it easy to make one of these triangles however tall you want.
void main() {
int height = 6;
for (int numberOfStars = 1; numberOfStars <= height; numberOfStars++) {
for (int i = 0; i < numberOfStars; i++) {
System.out.print("*");
}
System.out.println();
}
}
*
**
***
****
*****
******
Drawing Isosceles Triangles
Another fun shape is the isosceles triangle.
*
***
*****
For this one, the 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() {
System.out.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++) {
System.out.print(" ");
}
for (int i = 0; i < row * 2 - 1; i++) {
System.out.print("*");
}
System.out.println();
}
}
*
***
*****
*******
*********
Which can get tricky. For now, you can just study the code that does it for this shape. There will be more things to draw in the challenges section.1
The reason I'm focusing on this isn't because its likely you will get a job drawing shapes, but if you can draw a shape you can dodge a ball.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
You are extremely likely to be quizzed on for
loops on any standardized tests,
so these challenges are going to include a lot of repetition and sometimes a tricky
case to handle. Its for your own good, I hope.
Challenge 1
Write code that will output every number from 0 to 15, including 0 and 15.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
}
Do it using a for
loop and again using a while
loop.
Challenge 2
Write code that will output every number from 15 to 0, including 0 and 15.
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Do it using a for
loop and again using a while
loop.
void main() {
}
Challenge 3
Write code that will output every number from 15 to 0, excluding 0 and 15.
14
13
12
11
10
9
8
7
6
5
4
3
2
1
Do it using a for
loop and again using a while
loop.
void main() {
}
Challenge 4.
Write code that will output every third number from 51 to 66.
53
56
59
62
65
Do it using a for
loop and again using a while
loop.
void main() {
}
Challenge 5.
Draw a square.
Make it so that you can make the square bigger or smaller by changing a variable at the start of the program.
*****
*****
*****
*****
void main() {
}
Challenge 6.
Draw a rectangle.
Make it so that you can make the rectangle bigger or smaller in either dimension by changing a variable at the start of the program.
******
******
******
void main() {
}
Challenge 7.
Draw a circle!
Make it so that you can make the circle bigger or smaller by changing a variable at the start of the program.
**
****
******
********
******
****
**
void main() {
}
Challenge 8.
Draw a smiley face!
Make it so that you can make the smile bigger or smaller by changing a variable at the start of the program.
void main() {
}
Methods
All the code you have seen up until this point has lived inside of void main() {}
.
void main() {
System.out.println("CODE GO HERE");
}
This isn't sustainable for a few reasons. One is that code can get big. Putting a thousand lines inside of one place can be a lot, let alone the hundreds of thousands you need to be Minecraft or the millions you need to be Google.
Another is that there is almost always code that you will want to run at multiple places in a program. Copy pasting that code can get old.
This is what methods are for. Methods let you split up your code into smaller, reusable chunks.1
The word method comes from a "method of getting things done." You might also hear methods referred to as "functions".
Declaration
The simplest kind of method is declared by writing void
followed by some name, ()
, and some code inside of a {
and }
.
void doThing() {
System.out.println("Hello from inside a method!");
}
void main() {
doThing();
}
Invocation
Once you've declared a method you can run the code inside of it by writing the
name of the method followed by ()
in a statement.
void doThing() {
System.out.println("Hello from inside a method!");
}
void main() {
doThing();
}
Running the code in a method can be called a few things. "Running a method", "Calling a method", or "Invoking a method."1
You can call a method multiple times. If you do, then the code inside of it will be run multiple times.
void doThing() {
System.out.println("Hello from inside a method!");
}
void main() {
doThing();
doThing();
}
I like that last one because "invoking" makes me sound like a Wizard.
Scope
Methods can contain any code, including variable declarations.
void sayMathStuff() {
int x = 1;
int y = 2;
System.out.println("x is " + x);
System.out.println("y is " + y);
System.out.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;
System.out.println("x is " + x);
System.out.println("y is " + y);
System.out.println("x + y is " + (x + y));
}
void main() {
sayMathStuff();
// Error, x doesn't exist here
System.out.println(x);
}
This is why we have called variables "local variables." They are local to the "scope" of the method they are declared in.
main
The main
method works the same as any other method. Java just treats it special by choosing to
call it in order to start your programs.
void main() {
System.out.println("Java will start here");
}
This means you can do anything in your main
method you can do in any other method, including returning early.
void main() {
int x = 5;
if (x == 5) {
return;
}
System.out.println("WONT RUN");
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Declare a method named printSquare
. When invoked it should print a square.
*****
*****
*****
*****
// CODE HERE
void main() {
printSquare();
}
Challenge 2.
Declare a method named printSquareThreeTimes
. When invoked it should print three squares
by invoking a method named printSquare
three times.
// CODE HERE
// Code from challenge 1 here
void main() {
printSquareThreeTimes();
}
Challenge 3.
Write a program that contains at least four methods. Have fun with it.
void main() {
// sing a song or something.
}
Arguments
If methods always had to do the same thing each time they were run, they wouldn't be that useful.
The way to customize what happens when a method is called is to have them take "arguments."
void sayHello(String name) {
System.out.println("Hello " + name + "!");
}
void main() {
// Hello Joshua!
sayHello("Joshua");
// Hello Claire!
sayHello("Claire");
}
Declaration
To declare a method which takes arguments, instead of putting ()
after the method name
you need to put a comma separated list of argument declarations.
Each argument declaration looks the same as a variable declaration and has both a type and a name.
// This declares a single argument named "food" that
// has a type of "String".
void eat(String food) {
System.out.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) {
System.out.println(
"Happy " + age + "th birthday " + to + "!"
);
}
Invocation with Arguments
To invoke a method which takes arguments you need to, instead of writing
()
after the method name, write (
followed by a comma separated list
of literals or variable names ending with )
.
void eat(String food) {
System.out.println("I ate " + food);
}
void happyBirthday(String to, int age) {
System.out.println(
"Happy " + age + "th birthday " + to + "!"
);
}
void main() {
// This calls the 'eat' method with the String "cake"
// as an argument.
eat("Cake");
// You can also call methods using values stored in
// variables.
String veggie = "carrot";
eat(veggie);
// For more than one argument, you separate them with commas
happyBirthday("Charlotte", 24);
}
Reassignment
Inside of a method, arguments work the same as normal variable declarations. This means that their value can be reassigned within the method body;
void eat(String food) {
System.out.println("I ate " + food);
food = "nothing";
System.out.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) {
System.out.println("I ate " + food);
food = "nothing";
System.out.println("Now I have " + food);
}
void main() {
String fruit = "apple";
eat(fruit);
System.out.println(
"But in the caller I still have an " + fruit
);
}
Final Arguments
Just like normal variable declarations, arguments can be marked
final
. This makes it so that they cannot be reassigned.
void eat(final String food) {
System.out.println("I ate " + food);
}
void main() {
eat("Welsh Rarebit");
}
If you try to reassign a final argument, Java will not accept your program.
void eat(final String food) {
System.out.println("I ate " + food);
// Will not work
food = "toast";
System.out.println(food);
}
void main() {
eat("Welsh Rarebit");
}
This has the same use as regular final variables. If there are lots of lines of code where a variable might be reassigned, it can be useful to not have to read all that code to know that it does happen.1
Adding final
to all arguments can make it harder to read the code, simply because of visual noise.
Aliasing
Because arguments work like variables, if you pass something like an array as an argument the array referenced by the argument will be the exact same as the array referenced by the variable given to the method.
void incrementFirst(int[] numbers) {
numbers[0] = numbers[0] + 1;
}
void main() {
int[] nums = new int[] { 8 };
// The first number is 8
System.out.println(
"The first number is " + nums[0]
);
incrementFirst(nums);
// Now it is 9
System.out.println(
"Now it is " + nums[0]
);
}
The argument aliases the value passed to the method.
Overloading
Multiple methods can be declared that have the same name. This is allowed so long as each method takes different types or different numbers of arguments.
void doThing(int x) {
System.out.println(x);
}
void doThing(String name) {
System.out.println("Hello " + name);
}
void doThing(int x, int y) {
System.out.println(x + y);
}
When you call the method, Java will know what code to run because it knows the types of and number of arguments you are passing.
void doThing(int x) {
System.out.println(x);
}
void doThing(String name) {
System.out.println("Hello " + name);
}
void doThing(int x, int y) {
System.out.println(x + y);
}
void main() {
// Java can figure out what to do
doThing(1);
doThing("abc");
doThing(1, 2);
}
When there are multiple methods that have the same name but take different arguments, those methods are considered "overloads" of eachother1
"Overloading" in this context means when one word has more than one possible meaning depending on how it is used. Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.
Inferred Types
With variable declarations, you can use var
to let Java figure out the type
of the variable.
var name = "Jupiter";
This is not allowed with argument declarations.
// You aren't allowed to use var for arguments!
void makeHorchata(var milkFatPercent) {
// ...
}
You must always explicitly write out the types of arguments.
void makeHorchata(double milkFatPercent) {
System.out.println(
"Making a horchata with " + milkFatPercent + "% milk."
);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named printSquare
which takes one int
argument named size
.
The size
argument should control how big of a square is output.
// CODE HERE
void main() {
printSquare(4);
System.out.println();
printSquare(3);
System.out.println();
printSquare(2);
System.out.println();
printSquare(1);
System.out.println();
}
Challenge 2.
What happens if a negative number is given to your printSquare
?
Make it so that if a negative number is given, it works the same as if a positive number was given.
// CODE HERE
void main() {
printSquare(3);
System.out.println();
printSquare(-3);
System.out.println();
System.out.println();
printSquare(-2);
System.out.println();
printSquare(2);
}
Challenge 3.
Write a method with four overloads such that
the code in main
can run unchanged.
// CODE HERE
void main() {
f(2);
f("b");
f('9');
f(new String[] { "s" });
}
Challenge 4.
Call the defined methods in a way that outputs "I did it!"
void i() {
System.out.print("I");
}
void did(String what) {
System.out.println("did " + what);
}
void space() {
System.out.print(" ");
}
void main() {
// Code here
}
Return
If the only thing you could do with methods was to call them, they would have limited uses.
This is why methods can also "return" results to the code that calls them.
int plusOne(int x) {
return x + 1;
}
Declaration
The return type of a method is written to the left of the name of the method.
To have a method which returns an int
, you write int methodName
.
int returnsEight() {
return 8;
}
To have a method with returns a String
, you write String methodName
.
String getName() {
return "bob";
}
And so on for any type in Java.
Return Statement
Whenever the code in a method reaches a line that looks like return <VALUE>;
, that method
will immediately exit.
This will exit out of any loops, similarly to a break
.
int doMath() {
int x = 0;
while (true) {
x++;
if (x == 8) {
return x;
}
}
// Needed because Java isn't smart enough to know
// that the while loop will always reach the return x;
// line.
return 0;
}
We call this kind of line a return statement.
Exhaustiveness
When a method returns data, Java needs to know that no matter what happens
in the method there will be some return
line reached.
int compute(int x) {
if (x < 0) {
return 5;
}
// Error! No return if x >= 0
}
int compute(int x) {
// Both "branches" have returns, so all is well
if (x < 0) {
return 5;
}
else {
return 1;
}
}
We call this property, whether in every situation a method will return a value, "exhaustiveness." If there could be cases where no return statement is reached, that is "non-exhaustive" and Java won't accept your code.
void
All methods have to declare some return type. void
is what you write when a method won't return any value.
// Returns a String
String title() {
return "All Night";
}
// Returns an int
int views() {
return 4071;
}
// Doesn't return any value.
void talkAboutVideo() {
System.out.println(title() + " only has " + views() + " views.");
}
// This is what the void in "void main()" means
void main() {
talkAboutVideo();
}
Return in void methods
In void
methods, you can still exit early with a return
statement, but you do not give any
value after it.
void doStuff() {
int i = 0;
while (true) {
if (i == 8) {
return;
}
System.out.println(i);
}
}
Conversion
When a value is returned, Java will want to coerce it into the type of value that the method says it returns.
If Java knows how to do that conversion, then it can happen automatically.
// This method declares a return type of double.
double returnFive() {
// x is an int
int x = 5;
// When it is returned, it will be turned into a double
return x;
}
But if that conversion might potentially be lossy or, as with converting double
s to int
s,
you must do it yourself.
double returnNine() {
double nine = 9.0;
// The (int) explicitly converts the double to an int
return (int) nine;
}
Unreachable Statements
If Java can figure out that some line of code is unreachable because it will always come after a
return
, then it will not run your code.
void doThing() {
System.out.println("A");
return;
// unreachable statement
System.out.println("B");
}
void main() {
doThing();
}
Java is easy to trick though.1
void doThing() {
System.out.println("A");
if (true) {
return;
}
System.out.println("B");
}
void main() {
doThing();
}
This will always return before the println
, but Java chooses to not figure that out. It can't be smart enough to see through every if
, so it doesn't try for any of them.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a method that takes a String
as an argument and returns an int
as a result.
How the value for that int is determined is up to you.
// CODE HERE
void main() {
int x = process("abc");
System.out.println("Got " + x);
}
Challenge 2.
Define three methods such that the given main method will run.
// CODE HERE
void main() {
f(g(h(4), "b"), "e", "s");
}
Challenge 3.
Make the following multiply
method work for negative numbers.
Do this without simply multiplying using the *
operator.
int multiply(int x, int y) {
int total = 0;
for (int i = 0; i < y; i++) {
total += x;
}
return total;
}
void main() {
System.out.println(multiply(3, 5));
// System.out.println(multiply(-5, 5));
// System.out.println(multiply(-5, -2));
// System.out.println(multiply(9, -2));
}
Challenge 4.
Define a method, subtractInt
, which makes the following
code run and produce the "correct" result.
You will need to perform a narrowing conversion.
// CODE HERE
double add(double x, double y) {
return x + y;
}
double multiply(double x, double y) {
return x * y;
}
void main() {
int x = 5;
int y = 8;
int z = subtractInt(add(4, 5), mul(4, 2));
System.out.println(z);
}
Challenge 5.
null
There is a special value called null
which is assignable to most types.
void main() {
String name = null;
int[] numbers = null;
System.out.println(name);
System.out.println(numbers);
}
The only types which null
cannot be assigned to are int
, double
, char
, and boolean
.1
void main() {
// Will not work
int x = null;
}
As well as long
, short
, byte
, and float
but I haven't shown you those yet.
Null as Absence
One way to use null
is to have it be a stand in for when there is
an "absence" of a value.
Consider Cher. Unlike most people, Cher does not have a last name.
null
is an appropriate value to use when there is such an absense.
void main() {
String firstName = "Cher";
String lastName = null;
System.out.println(firstName);
System.out.println(lastName);
}
Null as Unknown
Another equally valid way to use null
is to have it
stand in for information that you do not yet know.
If you have a program ask someone for their name, between the time you start the program and you get a response, their name is unknown to you.
String firstName = null;
// Some lines of code
firstName = askForName();
This is subtly different than delayed assignment. Between when
you don't know the information and when you learn it you are actually allowed
to use a variable initialized to null
.
The difference between this kind of situation and a "known absence" is also subtle. In this situation you do not know what a value would be. In the other you know that there is no value to get.
Checking for null
If you are unsure whether something is null, you can
check by using ==
.
void sayHello(String firstName, String lastName) {
if (lastName == null) {
System.out.println("Hello " + firstName);
}
else {
System.out.println("Hello " + firstName + " " + lastName);
}
}
void main() {
sayHello("Sonny", "Bono");
sayHello("Cher", null);
}
NullPointerException
If you try to perform an operation on a null
reference,
such as checking the .length()
of a String
, your code will
crash.
void main() {
String thing = null;
// NullPointerException
System.out.println(thing.length());
}
When this happens the error that Java shows you will be a "NullPointerException
".
This will look something like the following.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "thing" is null
at Main.main(Main.java:4)
Because this is easy to make happen by mistake, it is worth familiarizing yourself with the format of it.
This way you can laser focus on the part that says something like "Cannot invoke "String.length()" because "thing" is null
" and know that the issue is with some variable named thing
that you are trying to call .length()
on.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method which takes in a String[]
representing
a series of names and prints out every name in sequence.
If this method is given null
, it should act as if it
was given an empty array.
void printNames(String[] names) {
}
void main() {
printNames(new String[] {
"Joker",
"Batman",
"Alfred"
});
}
Challenge 2.
Alter the method you wrote in the previous challenge
so that if it is given null
it outputs the message
You do not know any names yet
.
If it is given an empty String[]
it should continue
to simply output nothing.
void printNames(String[] names) {
}
void main() {
printNames(new String[] {
"Joker",
"Batman",
"Alfred"
});
}
Challenge 3.
Will the following code throw a NullPointerException
?
Why or why not?
void main() {
String[] jobs = new String[] {
"Carpenter",
"Baker",
null,
"Astronomer"
};
for (int i = 0; i < jobs.length; i++) {
System.out.println(jobs[i]);
}
}
Challenge 4.
The following code won't work. Give your best guess as to why and then try running it.
void main() {
int[] numbers = new int[] {
45,
32,
null,
94
};
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
}
Challenge 5.
Without changing anything in the main
method, make the bigness
method not throw a NullPointerException
and still have the "correct"
behavior for non-null inputs.
String bigness(String letters) {
int bigness = 0;
for (int i = 0; i < letters.length(); i++) {
bigness++;
}
if (bigness < 5) {
return "small";
}
else if (bigness < 10) {
return "medium"
}
else {
return "big";
}
}
void main() {
System.out.println(
bigness("bore")
);
System.out.println(
bigness("boiler")
);
System.out.println(
bigness("filter")
);
System.out.println(
bigness("knower")
);
System.out.println(
bigness("chrysanthemum")
);
System.out.println(
bigness(null)
);
}
Boxed Primitives
The fact that int
, double
, char
, and boolean
cannot have null
values
can be limiting.
For this reason there are versions of those primitive1 types which do not have this restriction.
void sayAge(Integer age) {
if (age == null) {
System.out.println("Age is not yet known");
}
else {
System.out.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 you they are made by taking the underlying thing and putting it in a "box."2
We call them "primitive" types because there isn't a way for you to implement them yourself in Java. They have to be given to you as a fundamental and magic sort of thing.
Don't worry too much about the word box in this context. This will make more sense once you learn how to define your own types. I just wanted to at least try to gesture at why it has the silly name that it does.
Integer
The type to use for an int
that might be null is Integer
.
void main() {
Integer i = null;
System.out.println(i);
i = 5;
System.out.println(i);
}
If you try to do any math on an Integer
which holds null
you will
get a NullPointerException
.
void main() {
Integer i = null;
System.out.println(i * 5);
}
Double
The type to use for a double
that might be null is Double
.
void main() {
Double d = null;
System.out.println(d);
d = 3.14;
System.out.println(d);
}
If you try to do any math on a Double
which holds null
you will
get a NullPointerException
.
void main() {
Double d = null;
System.out.println(d + 1);
}
Character
The type to use for a char
that might be null is Character
.
void main() {
Character c = null;
System.out.println(c);
c = '%';
System.out.println(c);
}
Unlike a char[]
, a Character[]
will not be have its contents
shown when printed.
void main() {
char[] c1 = new char[] { 'a', 'b', 'c' };
System.out.println(c1);
Character[] c2 = new Character[] { 'a', 'b', 'c' };
System.out.println(c2);
}
Boolean
The type to use for a boolean
that might be null is Boolean
.
void main() {
Boolean b = null;
System.out.println(b);
b = true;
System.out.println(true);
}
Unboxing Conversion
If you try to use a boxed primitive in a context where the normal type is expected, it will be implicitly "unboxed."
This means you can use Integer
s directly in math expressions.
void main() {
Integer x = 5;
int y = 3;
int z = x * y;
System.out.println(z);
}
As well as Boolean
s in logical expressions.
void main() {
Boolean hasHat = true;
if (hasHat) {
System.out.println("You have a hat!");
}
}
And so on for Double
, Character
, etc.
But if you use one of these types like this and they happen to be null
you will
get a NullPointerException
.
void main() {
Integer x = null;
// Bool
int y = x;
}
Boxing Conversion
If you try to assign to a boxed type like Integer
from some
code that gives you the unboxed version like int
, then Java will
automatically do that conversion.1
void main() {
int x = 5;
Integer y = x;
System.out.println(x);
System.out.println(y);
}
This might feel obvious, but this is one of the few places in Java where the type of something "magically" changes. int
and Integer
, char
and Character
, etc. are different types.
Arrays of Boxed Primitives
If you have an array of the boxed version of a type, that is not compatible with an array containing the unboxed version and vice-versa.1
int[] numbersOne = { 1, 2, 3 };
Integer[] numbersTwo = { 4, 5, 6 };
// This line won't work
numbersOne = numbersTwo;
// And neither will this one
numbersTwo = numbersOne;
This means that to turn something like a boolean[]
into a Boolean[]
or vice-versa,
you must manually make a new array and copy over elements. Doing this in either
direction will work because boxing and unboxing conversions exist between the primitives
and their boxed variants.
void main() {
boolean[] yesAndNo = new boolean[] { true, false };
Boolean[] yesAndNoCopy = new Boolean[] { false, false };
for (int i = 0; i < yesAndNo.length; i++) {
// Here a boxing conversion takes place
yesAndNoCopy[i] = yesAndNo[i];
}
boolean[] yesAndNoCopyCopy = new boolean[] { false, false };
for (int i = 0; i < yesAndNoCopy.length; i++) {
// And here an unboxing conversion
yesAndNoCopyCopy[i] = yesAndNoCopy[i];
}
}
The reasons for this are deeply interesting and have to do with the nitty gritty of how Java is actually implemented. It might also change in the future.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Will this code run? Why or why not.
int compute(int x) {
if (x == 0) {
return null;
}
else {
return x * x;
}
}
void main() {
System.out.println(compute(5));
}
Challenge 2.
Write a method which takes in a Integer[]
representing
a series of distances and prints out every distance
followed by kilometers
.
So if the array has 1
, 2
, and 3
you should output
1 kilometers
2 kilometers
3 kilometers
If this method is given null
, it should act as if it
was given an empty array.
void printDistances(Integer[] distances) {
}
void main() {
printNames(new String[] {
45,
99,
23
});
}
Challenge 3.
Write a method called onlyPositive
which takes in an int
and returns
the same value out if the number is greater than zero.
If the number is less than or equal to zero, return null
.
// Write onlyPositive here
void main() {
// 45
System.out.println(
onlyPositive(45)
);
// 46
System.out.println(
onlyPositive(45) + 1
);
// null
System.out.println(
onlyPositive(0)
);
// null
System.out.println(
onlyPositive(-1)
);
}
Challenge 4.
Will the following code work? Why or why not?
void main() {
int ducks = 5;
Integer sparrows = 3;
int birds = ducks + sparrows;
System.out.println(birds);
}
Challenge 4.
Will the following code work? Why or why not?
void main() {
char[] face = new char[] { ':', ')' };
Character[] smile = face;
System.out.println(smile);
}
Challenge 5.
Will the following code work? Why or why not?
void main() {
char[] face = new char[] { ':', ')' };
Character[] smile = new Character[face.length];
for (int i = 0; i < face.length; i++) {
smile[i] = face[i];
}
System.out.println(smile);
}
Arrays II
Fairly often you will want to have arrays in your program which you either do not know the initial values for or which are too big to physically type out every value in an initializer.
String[] everyStudentName = // ???
char[] everyLetterInEveryAlphabet = // ???
Initializion with Size
The Nintendo GameBoy had a screen resolution of 160 x 144. To store the value of each pixel1 you would need an array 23,040 items long.
To support this without you writing the word false
23,040 times,
arrays can be made with just by giving a size and skipping the initializer.
boolean[] pixels = new boolean[23040];
So you have to say new
followed by the type of element in the array, [
, the size of the array and ]
.
The original GameBoy wasn't actually just black and white. It supported 7 shades of gray, so a boolean
wouldn't technically to be enough to represent a pixel's state. You'd have to use something with at least 8 states, not just 2.
Default Values
When an array is made by just providing a size, its elements are initialized to some default value.
For primitive types like int
and double
, each element will be initialized to 0
.
void main() {
int[] digits = new int[10];
// 0
System.out.println(digits[0]);
double[] readings = new double[5];
// 0.0
System.out.println(readings[0]);
}
For boolean
, each element will be initialized to false
.1
void main() {
boolean[] pokedex = new boolean[10];
// false
System.out.println(pokedex[0]);
}
And for every non-primitive type, which is every single type including the boxed primitives,
the default value will be null
.
void main() {
String[] names = new String[10];
// null
System.out.println(names[0]);
Integer[] scores = new Integer[26];
// null
System.out.println(scores[0]);
}
Fun fact. The GameBoy and GameBoy Advance Pokemon games tracked pokedex completion in a big boolean
array. If you saw a Pokemon it would flip that Pokemon's index in the "seen" array to true
. If you caught it, it would do the same in a different array. Those games weren't written in Java, but the concept is the same.
Populate Arrays
If the default value for an array is not valid for what you are doing, you will need to populate the array with better initial values.
For loops are generally good for this purpose.
void main() {
char[] letters = new char[26];
for (int i = 0; i < letters.length; i++) {
letters[i] = (char) ('a' + i);
}
System.out.println(letters);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make an empty String
array without using an empty initializer.
This means you cannot write String[] empty = {}
or String[] empty = new String[] {}
.
void main() {
String[] empty = ???;
// Should be 0
System.out.println(empty.length);
}
Challenge 2.
What will the following code output? Change the code between the lines so it instead outputs the following.
1.0
2.0
3.0
4.0
5.0
void main() {
Double[] prices = new Double[5];
// ----------
// CODE HERE
// ----------
for (int i = 0; i < prices.length; i++) {
double price = prices[i];
System.out.println(price);
}
}
Challenge 3.
Only writing code between the lines and without reassigning the sandwich
variable,
make the following code output egg and cheese
.
void main() {
char[] sandwich = new char[14];
// ----------
// CODE HERE
// ----------
System.out.println(sandwich);
}
Challenge 4.
Populate the triangle
array such that the code
prints a right triangle that looks like the following.
*
**
***
void main() {
char[] triangle = new char[8];
// ----------
// CODE HERE
// ----------
System.out.println(triangle);
}
Challenge 5.
Make a method named buildTriangle
which returns a char[]
that can be printed out to display a right triangle of any height.
You can ignore the possibility that a negative or zero height is given.
char[] buildTriangle(int height) {
// CODE HERE
}
void main() {
System.out.println(buildTriangle(3));
System.out.println("--------------");
System.out.println(buildTriangle(5));
System.out.println("--------------");
System.out.println(buildTriangle(2));
}
Classes
Up until now all the data types you have used - int
, String
, etc. -
came with Java. This works for awhile, but eventually you will need to define your own types.
The way to do this is with a "class."
class Person {
}
The meaning of Class
Classes are descriptions of a "classification" of objects in your program.
The terminology is analagous to biological classification.
Where Plato would classify any "featherless biped" as a human1,
a String
is classified by the set of things you can do to it and the data
it stores. The String
class would specify all of that.2
What an idiot
If thats a bit too heady of an explanation don't fret. You will likely get it intuitively eventually. There are like 50 different ways to explain it and eventually one will land for you.
Class Declaration
To declare a class, you say class
followed by a name for the class and a pair of {
and }
class Muppet {
}
Naming
It is social convention to name classes with the first letter of each word capitalized. So if you wanted to make a class representing an inch worm, you would say the following.1
class InchWorm {
}
For things that are not English or are acronyms the rules get fuzzy. Use your best judgement.
Instances
Once you've declared a class, you can make an instance
of that class by saying new
followed by that class's name
and ()
.1
class Muppet {
}
void main() {
Muppet kermit = new Muppet();
System.out.println(kermit);
}
Very similarly to arrays, the output from printing an instance of a class might seem like gibberish (Main$Muppet@1be6f5c3
).
You will learn how to make it nicer later.
I haven't used it in many code samples thus far, but if you remember var
this is one of the times
where it can be aesthetically convenient. var kermit = new Muppet();
Fields
Classes contain zero or more fields.
Fields are like variables except they don't live in a method, they are attached to instances of the class they are a part of.
To declare a field in a class you say the type of the field, a name for the field,
and then a ;
.
class Muppet {
String name;
}
One way to think about it is that when you say new Muppet()
, Java makes a box big enough to hold
all of the fields that a muppet needs.1
This "box" metaphor is part of where the name "boxed primitives" comes from.
Field Initialization
You can set an initial value for a field in a few ways.
One is to access to assign the field directly on the instance created.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
}
Another is to set a default value directly in the field declaration. This makes the most sense if its a value that most instances will share.
class Muppet {
// Most are!
boolean talented = true;
}
void main() {
Muppet kermit = new Muppet();
}
Field Access
You can access the value of any field on 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
System.out.println(kermit.name);
}
Field Default Values
Before a field in a class is given a value it will have the same default value as it would if you made an array of the field's type.
That is: 0
for int
, 0.0
for double
, false
for boolean
, and null
for most all else.
class Muppet {
int age;
double salary;
boolean talented;
String name;
}
void main() {
Muppet kermit = new Muppet();
// 0
System.out.println(kermit.age);
// 0.0
System.out.println(kermit.salary);
// false
System.out.println(kermit.talented);
// null
System.out.println(kermit.name);
}
Aliasing
When two variables point to the same instance of a class, those variables will be aliases for the same data.
This means that, just like arrays, if you change the value of a field on one that change will be visible on the other.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
Muppet darkKermit = kermit;
kermit.name = "Kermit The Frog";
// Kermit The Frog
System.out.println(kermit.name);
// Kermit The Frog
System.out.println(darkKermit.name);
}
Return Multiple Values
Methods can only ever return one type of thing. This
usually retricts you to only ever returning one int
or
one String
.
But if you make a class that has multiple fields you can use that to return more than one piece of information.1
class Location {
double latitude;
double longitude;
}
Location findTreasureIsland() {
Location location = new Location();
location.latitude = 40.2085;
location.longitude = -3.713;
return location;
}
void main() {
Location treasureIsland = findTreasureIsland();
System.out.println(
"Treasure island is located at " +
treasureIsland.latitude +
" " +
treasureIsland.longitude +
"."
);
}
There are many more reasons to make your own classes, but this one is pretty quick to see even at this stage.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
The following classes are named "wrong." Name them correctly.
class gonzo {}
class fozzie_the_bear {}
class MSPIGGY {}
void main() {
System.out.println(new gonzo());
System.out.println(new fozzie_the_bear());
System.out.println(new MSPIGGY());
}
Challenge 2.
Make a variable named movie
which is an instance of the Movie
class.
Set the value of its title
field to Muppets in Space
.
class Movie {
String title;
}
void main() {
// ---------------
// CODE HERE
// ---------------
System.out.println(
movie.title
);
}
Challenge 3.
Alter the ThemePark
class so that the default value
for its entranceFee
field is 35.24
.
class ThemePark {
double entranceFee;
}
void main() {
ThemePark themePark = new ThemePark();
System.out.println(
themePark.entranceFee
);
}
Challenge 4.
Changing only the indicated line,
make it so the word Kermit
only appears twice in the
program.
Hint: Remember that inferred types exist.
class Kermit {
boolean angry = true;
}
void main() {
// ------------------------
// CHANGE ONLY THIS LINE v
Kermit kermit = new Kermit();
// ------------------------
System.out.println(kermit.angry);
}
Challenge 5.
Make a method named squareRoot
which returns an
instance of the SquareRoot
class containing both
the positiveRoot
and negativeRoot
of the given double
.
So if you are given 4
you should return a positive root
of 2
and a negative root of -2
.
You do not have to account for the possibility of being given a negative
number. You should use Math.sqrt
to find the positive root and common
sense to find the negative root.
class SquareRoot {
double positiveRoot;
double negativeRoot;
}
void squareRoot(double value) {
// -----------
// CODE HERE
// -----------
}
void main() {
SquareRoot sqrtOfFour = squareRoot(4);
// 2
System.out.println(sqrtOfFour.positiveRoot);
// -2
System.out.println(sqrtOfFour.negativeRoot);
SquareRoot sqrtOfFifteen = squareRoot(15);
// 3.872983346207417
System.out.println(sqrtOfFifteen.positiveRoot);
// -3.872983346207417
System.out.println(sqrtOfFifteen.negativeRoot);
}
Challenge 6.
Only writing code between the lines and without directly accessing any fields on or reassigning the actor
variable, make the program output Tim Curry
.
Hint: The key word is "directly."
class Actor {
String name;
}
void main() {
Actor actor = new Actor();
actor.name = "Frog, Kermit the";
// --------------------------
// CODE HERE
// --------------------------
System.out.println(actor.name);
}
Instance Methods
In addition to having fields, classes can also have their own method definitions.
These look the same as the method definitions you've seen so far, they are just put within a class definition.1
class Muppet {
String name;
void freakOut() {
System.out.println("**ANGRY KERMIT NOISES**")
}
}
We call these instance methods because you need an instance of the class in order to call the method.
If you haven't seen the muppets this might have go over your head, but Kermit randomly gets really mad.
Invocation
To invoke an instance method you first need an instance of the class.
You then write .
followed by the name of the instance method and a list of arguments.1
class Elmo {
void talkAboutRocko() {
System.out.println("ROCKO'S NOT ALIVE!!")
}
}
void main() {
Elmo elmo = new Elmo();
elmo.talkAboutRocko();
}
Daily reminder that Elmo absolutely hates Rocko
Arguments
Instance methods can take arguments the same as the methods you have seen so far.
class Muppet {
String name;
void singLyric(int verse) {
if (verse == 1) {
System.out.println("Why are there so many");
}
else if (verse == 2) {
System.out.println("Songs about rainbows");
}
else {
System.out.println("And what's on the other side?");
}
}
}
Field Access
Within an instance method's definition, you can access the values of any fields declared in the class by just writing their name.
class Elmo {
int age;
void sayHello() {
System.out.println("Hi, I'm Elmo");
System.out.print("I am ");
// You can use elmo's age by just writing "age"
System.out.print(age);
System.out.println(" years old.");
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.sayHello();
}
Field Updates
You can also update the value of any field from within an instance method the same as if it were a local variable.
class Elmo {
int age;
void sayHello() {
System.out.println("Hi, I'm Elmo");
System.out.print("I am ");
System.out.print(age);
System.out.println(" years old.");
}
void haveBirthday() {
age = age + 1;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.sayHello();
elmo.haveBirthday();
elmo.sayHello();
}
Derived Values
A common use for methods is to provide a value that is derived from the values of other fields.
class Elmo {
int age;
int nextAge() {
return age + 1;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
System.out.println("Elmo is " + elmo.age + " right now,");
System.out.println("but next year Elmo will be " + elmo.nextAge());
}
Which is useful for situations like where you store someones first and last name
Invoke Other Methods
Inside of an instance method you can invoke other instance methods on the same class by writing their name and providing any needed arguments.
class Elmo {
int age;
void sayHello() {
System.out.println("Hi, I'm Elmo");
System.out.print("I am ");
System.out.print(age);
System.out.println(" years old.");
}
void startTheShow(String showName) {
sayHello();
System.out.println("Welcome to " + showName);
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.startTheShow("Sesame Street");
}
this
Within an instance method, you have access to a magic variable called this
.
this
is a variable that contains the current instance of the class.
You can use this
to access fields or invoke any method.
class Elmo {
int age;
void sayHello() {
System.out.println("Hi, I'm Elmo");
System.out.print("I am ");
System.out.print(this.age);
System.out.println(" years old.");
}
void startTheShow(String showName) {
this.sayHello();
System.out.println("Welcome to " + showName);
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.startTheShow("Sesame Street");
}
Disambiguation
One reason you might need to use this
is if the name
of an argument to a method is the same as the name of a field.
class Elmo {
int age;
boolean isOlderThan(int age) {
return this.age > age;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
// true
System.out.println(elmo.isOlderThan(2));
}
If you didn't do this, it would be ambiguous whether you were referring to the field or the argument. This removes the ambiguity.1
Really it isn't ambiguous for Java. It will just think you are referring to the argument. It is ambiguous from the perspective of a human being reading the code though.
Clarity
Another reason you might want to use this
is for code clarity.
It is a lot easier to know that a particular line refers to a field
or method on the same class if there is an explicit this.
before
the reference.1
class Elmo {
int age;
void sayHello() {
System.out.println("Hi, I'm Elmo");
System.out.print("I am ");
System.out.print(this.age);
System.out.println(" years old.");
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.sayHello();
}
This is very much a personal preference thing. I generally choose to write
this.
whenever I am able to for this reason.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a class named PirateShip
which has one int
field
named crewSize
.
Add an instance method to that class named sail
which outputs
N sailors, ready to sail!
Where N is the size of the crew.
// ----------------
// CODE HERE
// ----------------
void main() {
PirateShip ship = new PirateShip();
ship.crewSize = 25;
ship.sail();
}
Challenge 2.
Make a class named StringArrayView
which has
one String[]
field named value
and two methods named get
and length
.
get
should take in an index and return the matching element
of the array.
length
should take no arguments and give the length of the array.
// ----------------
// CODE HERE
// ----------------
void main() {
StringArrayView view = new StringArrayView();
view.value = new String[] { "A", "B", "C" };
// 3
System.out.println(view.length());
// A
System.out.println(view.get(0));
// C
System.out.println(view.get(2));
}
Challenge 3.
Alter the VoiceActor
class so that it has a method named fullName
that returns their firstName
followed by their lastName
and separated
by a space.
If their lastName
is null
, you should have no trailing space.
If their firstName
is null
, you should have no leading space.
If both their firstName
and lastName
are null
, you should
return "No Name"
.
class VoiceActor {
String firstName;
String lastName;
// -----------------
// CODE HERE
// -----------------
}
void main() {
VoiceActor goku = new VoiceActor();
goku.firstName = "Masako";
goku.lastName = "Nozawa";
// "Masako Nozawa"
String gokuFullName = goku.fullName();
System.out.println(gokuFullName);
// "Nozawa"
goku.firstName = null;
gokuFullName = goku.fullName();
System.out.println(gokuFullName);
// "No Name"
goku.lastName = null;
gokuFullName = goku.fullName();
System.out.println(gokuFullName);
// "Horikawa"
VoiceActor vegeta = new VoiceActor();
vegeta.lastName = "Horikawa";
System.out.println(vegeta.fullName());
}
Challenge 4.
Make a Rectange
class which has a width
field and a height
field. Give it an instance method named toCharArray
which gives
a char[]
that can be printed to display a rectangle of the given
width and height.
// ------------
// CODE HERE
// ------------
void main() {
Rectangle rectangle = new Rectangle();
rectangle.width = 3;
rectangle.height = 4;
/*
***
***
***
***
*/
char[] c = rectangle.toCharArray();
System.out.println(c);
}
Challenge 5.
Update the definition for the Taco
class so that it has a method named
deluxe
. This should set the taco to have beef, sour cream, cheese,
and onion. Use the existing instance methods instead of directly accessing
fields.
class Taco {
boolean beef;
boolean sourCream;
boolean cheese;
boolean onion;
void addBeef() {
this.beef = true;
}
void addSourCream() {
this.sourCream = true;
}
void addCheese() {
this.cheese = true;
}
void addOnion() {
this.onion = true;
}
void deluxe() {
// ------------
// CODE HERE
// ------------
}
}
void main() {
var taco = new Taco();
taco.deluxe();
System.out.println("Has Beef: " + taco.beef);
System.out.println("Has Sour Cream: " + taco.sourCream);
System.out.println("Has Cheese: " + taco.cheese);
System.out.println("Has Onion: " + taco.onion);
}
Challenge 6.
Why doesn't this code function as you'd expect? Fix it by changing one line.
class Oscar {
boolean grouchy;
void setGrouchy(boolean grouchy) {
grouchy = grouchy;
}
}
void main() {
var oscar = new Oscar();
oscar.setGrouchy(true);
System.out.println(oscar.grouchy);
}
Enums
While you can use String
, int
, boolean
, and friends
alongside your own custom classes to represent many situations,
that is not always enough.
Consider a stop light. At any given time it is either red, yellow, or green.
The tool we use to model that kind of thing is enums.
enum StopLight {
RED,
YELLOW,
GREEN
}
Enums are types with fixed sets of allowed values. We call them enums because they enumerate multiple different possibilities.
Declaration
To declare an enum
, you write enum
followed by a name for the enumeration
and the names of the different possibilities separated by commas.
enum StopLight {
RED,
YELLOW,
GREEN
}
Variants
We call the different possibilities that an enum represents its "variants."
So in the following code we would say that the TirePressure
enum has three variants: LOW
, NORMAL
,
and HIGH
.
enum TirePressure {
LOW,
NORMAL,
HIGH
}
Naming
You are expected to name enums the same as you would regular classes: WithCapitals
.
The variants of enums are expected to be named in all capital letters, with underscores1
enum CarSeat {
DRIVER,
SHOTGUN,
BACK_LEFT,
BACK_RIGHT
}
We call this convention "SCREAMING_SNAKE_CASE
", which is silly. Snakes can't speak, let alone scream. I should know.
Usage
To use an enum, first make a variable or field whose type is the name of the enum. Then assign to it one of the variants.
You can access the variants of an enum by writing the enum's name,
a .
, then the name of the variant.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight light = StopLight.RED;
System.out.println(
"The light is " + light
);
}
Equality
You can check if two enums contain the same variant
using the ==
operator.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight light = StopLight.RED;
if (light == StopLight.RED) {
System.out.println("You must stop");
}
else {
System.out.println("Full speed ahead!");
}
}
Comparison to boolean
Enums are very similar in spirit to boolean
s.
A boolean
has one of two values. true
or false
.
An enum also has one of a fixed set of values. The difference is that each item in this fixed set can have its own name and that there might be more than two values.
Depending on context and personal taste, it might even make sense to use an enum with two variants as a replacement for a boolean.
enum Power {
ON,
OFF
}
void main() {
Power power = Power.ON;
if (power == Power.ON) {
System.out.println("The power is on");
}
else {
System.out.println("The power is off");
}
}
The benefit here being that the names you give to the enum and to the variants
might be clearer to read in code than boolean
, true
, and false
.
The downside being that you needed to write more code.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make an enum named Response
which
has three variants. YES
, NO
, and MAYBE_SO
.
// -------------
// CODE HERE
// -------------
void main() {
System.out.println(
Response.YES
);
System.out.println(
Response.NO
);
System.out.println(
Response.MAYBE_SO
);
}
Challenge 2.
Write a method named goodPerformer
which takes
in a String
representing the name of an artist.
If that String
is equal to Pitbull
or Billy Joel
return YES
. If it is equal to Shaggy
return NO
.
Otherwise return MAYBE_SO
.
Use the enum you defined above.
// ------------
// CODE HERE
// ------------
void main() {
Response pitbull = goodPerformer("Pitbull");
System.out.println(pitbull);
Response billyJoel = goodPerformer("Billy Joel");
System.out.println(billyJoel);
Response shaggy = goodPerformer("Shaggy");
System.out.println(shaggy);
Response chappelRoan = goodPerformer("Chappell Roan");
System.out.println(chappelRoan);
}
Challenge 3.
Make a method named transition
which takes in a StopLight
and returns the next light it will transition to.
For those who don't drive cars: red lights go to green, green lights go to yellow, and yellow lights go to red.
enum StopLight {
RED,
YELLOW,
GREEN
}
StopLight transition(StopLight current) {
// ------------
// CODE HERE
// ------------
}
void main() {
var light = StopLight.RED;
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
}
Strings II
If you haven't already, you will eventually realize that String
s are
one of the most common data types you will use.
As such, its worth it to keep revisiting the things you can do with them. Expect more sections like this the deeper you get.
lowercase
In English1 letters can be either lower-cased (a
, b
, c
) or upper-cased (A
, B
, C
).
If you have a String
which potentially contains upper-cased letters, you can get a new String
with everything
transformed into lower-case using the .toLowerCase()
method.
void main() {
String message = "Happy Valentines Day";
String lowerCased = message.toLowerCase();
System.out.println(lowerCased);
}
This does not change the original String
in place. It just makes a new String
with all lower-case letters.
Other languages also have a notion of case. I am not a polyglot though, so I'm not qualified to talk about them.
UPPERCASE
Similarly, if you have a String
which potentially contains lower-cased letters, you can get a new String
with everything
transformed into lower-case using the .toUpperCase()
method.
void main() {
String message = "Happy Valentines Day";
String upperCased = message.toUpperCase();
System.out.println(upperCased);
}
This does not change the original String
in place. It just makes a new String
with all upper-case letters.
Equality ignoring case
If you want to check if two String
s contain the same contents
but you do not care if those contents differ in the casing of letters, you can use the .equalsIgnoreCase
method.
void main() {
String historicalFigureOne = "St. Valentines";
String historicalFigureTwo = "st. valentines";
System.out.println(
historicalFigureOne.equalsIgnoreCase(historicalFigureTwo)
);
}
This is different from the result the .equals
method will give you. That method will return false
if there
are any differences in the two String
s.
Check if empty
You can check if a String
is empty in a few ways.
The one you should already have been able to figure out1 is that you can get the .length
of a String
and see if that is zero.
void main() {
String textMessages = "";
System.out.println(
textMessages.length() == 0
);
}
But another is to use the explicitly defined .isEmpty()
method.
void main() {
String textMessages = "";
System.out.println(
textMessages.isEmpty()
);
}
This can be more convenient. Both to write, as it is fewer characters to type, and to read later on.
Again, no shame if not. I didn't exactly call attention to it.
Check if blank
You can check if a String
is blank by using the .isBlank
method.
The difference is that an empty String
has actually zero characters. A blank String
is can have characters, so long as those characters are what we would consider whitespace.
That is, things like spaces and newlines.
void main() {
String brainSounds = """
""";
// false
System.out.println(brainSounds.isEmpty());
// true
System.out.println(brainSounds.isBlank());
}
Strip extra whitespace
If you have a String
which might contains some extra "trailing" whitespace or extra "leading"
whitespace, you can remove that by using the .strip
method.
This will give a new String
with both the leading and trailing whitespace removed.
void main() {
String message = " Happy Valentines Day. ";
System.out.print(message.strip());
System.out.println("|");
}
If you want to just remove the leading whitespace, you can use .stripLeading
.
void main() {
String message = " Happy Valentines Day. ";
System.out.print(message.stripLeading());
System.out.println("|");
}
And to remove only trailing whitespace, .stripTrailing
.
void main() {
String message = " Happy Valentines Day. ";
System.out.print(message.stripTrailing());
System.out.println("|");
}
All of these methods are useful when you get input from human beings. Humans are generally pretty bad at seeing if they hit the spacebar one too many times.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named isUpperCase
which tells you if a
given String
is already made of all upper-case characters.
Hint: One way to do this is to call .toUpperCase
and check
if the result is the same as the input you were given.
boolean isUpperCase(String s) {
// -----------
// CODE HERE
// -----------
}
void main() {
// true
System.out.println(isUpperCase("ABC"));
// false
System.out.println(isUpperCase("abc"));
// false
System.out.println(isUpperCase("AbC"));
}
Challenge 2.
Do the same as above, but check if a given String
is
made of all lower case letters.
boolean isLowerCase(String s) {
// -----------
// CODE HERE
// -----------
}
void main() {
// false
System.out.println(isLowerCase("ABC"));
// true
System.out.println(isLowerCase("abc"));
// false
System.out.println(isLowerCase("AbC"));
}
Challenge 3.
Add an instance method named scream
to the Muppet
class. This
should replace the name of the muppet with the name in upper case
letters + an exclamation point (!
) at the end.
class Muppet {
String name;
// -------------
// CODE HERE
// -------------
}
void main() {
var kermit = new Muppet();
kermit.name = "kermit";
// kermit
System.out.println(kermit.name);
// KERMIT!
kermit.scream();
System.out.println(kermit.name);
}
Challenge 4.
Write a method called echo
.
If echo
is given a non-blank String
, it should print You Said: <...>
where <...>
is the String
they
gave minus any leading or trailing whitespace.
If echo
is given a blank
String
, it should print You Didn't Say Anything
.
void echo(String s) {
// -------------
// CODE HERE
// -------------
}
void main() {
// You Said: Hello
echo("Hello");
// You Said: Hello
echo(" Hello ");
// You Didn't Say Anything
echo("");
// You Didn't Say Anything
echo(" ");
}
Exceptions
When you do something that Java doesn't know how to handle, like dividing a number by zero, your program will fail.
The way that this happens is that an "exception" is thrown.
Say that in "normal conditions" code will proceed top to bottom, line by line. In "exceptional conditions" code will no longer be able to proceed.
void main() {
int x = 5 / 0;
System.out.println("Won't get here, an exception will occur");
}
throw
In order to throw an exception from your own code, you say throw
, new
, then the name
of the exception and ()
.
RuntimeException
is one of many kinds of exceptions, but you can make do with only
that in your own code for a bit.
void crashesOnFive(int x) {
if (x == 5) {
throw new RuntimeException();
}
}
void main() {
crashesOnFive(1);
System.out.println("Made it to step 1");
crashesOnFive(5);
System.out.println("Will not make it to step 2");
}
Messages
You can attach a message to an exception to give context to whoever sees the program crash as to why that happened.
You do this by putting a String
in the parentheses when throwing
your exception.
void crashesOnFive(int x) {
if (x == 5) {
throw new RuntimeException("5 is an evil number");
}
}
void main() {
crashesOnFive(1);
System.out.println("Made it to step 1");
crashesOnFive(5);
System.out.println("Will not make it to step 2");
}
Stack Traces
When you get an exception, it will contain what is known as a "stack trace."
If a method a
calls a method b
which in turn calls a method c
, that chain of
calls forms a "stack."
If the method at the bottom, c
, throws an exception that exception will contain
information about that entire "call stack."
void c() {
throw new RuntimeException();
}
void b() {
c();
}
void a() {
b();
}
void main() {
a();
}
Exception in thread "main" java.lang.RuntimeException
at Main.c(Main.java:2)
at Main.b(Main.java:6)
at Main.a(Main.java:10)
at Main.main(Main.java:14)
This makes exceptions somewhat offensive to the eyes, but is extremely useful if something goes wrong in a real program. You can see exactly what method had an issue and figure out where it was called from.
Since figuring out what went wrong is a detective game, every clue helps.
try/catch
If you know that some code might fail, and you have an idea of what to do if it does,
you can prevent an exception from crashing your program by catch
-ing it.
To do this, you write try
and put the code that might fail inside of {
and }
.
try {
mightFail();
}
Then you write catch
and in parentheses the kind of exception you want to handle as well as a variable name.1
try {
mightFail();
} catch (RuntimeException e) {
}
And inside the catch block you can write code to run when such an exception occurs.
void doThing(int x) {
if (x == 0) {
throw new RuntimeException("Cannot do something zero times");
}
}
void main() {
int x = 0;
try {
doThing(x);
} catch (RuntimeException e) {
System.out.println("Something went wrong doing a thing.");
}
}
Just as you cannot have an else
without an if
, you cannot have a catch
without a try
.
void main() {
catch (RuntimeException e) {
System.out.println("Hello");
}
}
Nor can you have a try
without a catch
.2
void main() {
try {
System.out.println("Hello");
}
}
Generally you will just use e
for this. Even if you don't call any instance methods on the exception, you still need to give a name for it.
Technically you can have a try
without a catch
, but only when using another feature of
Java you haven't been shown yet. It will make sense when the time comes.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named arise
which accepts a String
parameter
representing someone's name and prints Awake <NAME>
.
If this function is given an empty string throw a RuntimeException
.
// ------------
// CODE HERE
// ------------
void main() {
arise("Lion El'Jonson");
arise("Roboute Guilliman");
arise("");
}
Challenge 2.
Starting with the code you wrote above, make the thrown runtime exception include a message saying why it was thrown ("given an empty string" or something like that.)
// ------------
// CODE HERE
// ------------
void main() {
arise("Lion El'Jonson");
arise("Roboute Guilliman");
arise("");
}
Challenge 3.
The following code is written in an intentionally confusing way. Instead of reading the code and trying to figure it out that way, run the code and read the stack trace to figure out which method originally throws an exception.
void a(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
b(x / 2);
}
}
void b(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
c(x * 3 + 5);
}
}
void c(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
d(x / 4);
}
}
void d(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
e(x / 3);
}
}
void e(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
a(x / 10);
}
}
void main() {
a(1215135236);
}
Challenge 4.
Write a method named command
which takes in a SpaceMarine
.
If the space marine is corrupted throw a RuntimeException
.
Otherwise print out their name.
class SpaceMarine {
boolean corrupted;
String name;
}
// ---------------------
// CODE HERE
// ---------------------
void main() {
SpaceMarine titus = new SpaceMarine();
titus.corrupted = false;
titus.name = "Demetrian Titus";
command(titus);
SpaceMarine imurah = new SpaceMarine();
imurah.corrupted = true;
imurah.name = "Imurah";
command(imurah);
}
Challenge 5.
Alter your code above by adding a new method named safeCommand
. It should
call command
in a try/catch
block. If a RuntimeException
is thrown
it should print Unable to command
.
class SpaceMarine {
boolean corrupted;
String name;
}
// ---------------------
// CODE HERE
// ---------------------
void main() {
SpaceMarine titus = new SpaceMarine();
titus.corrupted = false;
titus.name = "Demetrian Titus";
command(titus);
safeCommand(titus);
SpaceMarine imurah = new SpaceMarine();
imurah.corrupted = true;
imurah.name = "Imurah";
safeCommand(imurah);
}
Switch
if
and else
let you branch logic based on whether any arbitrary
expression that evaluates to a boolean.
This is powerful because it lets you write logic as complicated as you need to.
if (isLeapYear && !bloodMoon && (age > 30 || catName.equals("fred"))) {
startRitual();
}
But it can be burdensome if all you are doing is checking if some variable has a particular value.
void main() {
if (food.equals("apple")) {
System.out.println("Red");
}
else if (name.equals("grape")) {
System.out.println("Purple");
}
else if (food.equals("orange")) {
System.out.println("Orange");
}
else {
System.out.println("Other");
}
}
For these situations, you can use a switch
.
switch (fruit) {
case "apple" -> {
System.out.println("Red");
}
case "grape" -> {
System.out.println("Purple");
}
case "orange" -> {
System.out.println("Orange");
}
default -> {
System.out.println("Other");
}
}
Case and Default
For a switch statement you write switch
followed by an expression in parentheses
and a list of cases in curly-braces.
Each case consists of a "case label" (case
followed by a literal value), an arrow (->
), and a
body to execute when the value given to the switch matches the value declared in the case.
The final case label can just be the word default
. This will match if none of the previous cases did
and gives you a place to put "default" behavior.
void sayColor(String fruit) {
switch (fruit) {
case "apple" -> {
System.out.println("Red");
}
case "grape" -> {
System.out.println("Purple");
}
case "orange" -> {
System.out.println("Orange");
}
default -> {
System.out.println("Other");
}
}
}
void main() {
sayColor("grape");
}
Strings
As already shown, you can use a case to match String
values.
void main() {
String veggie = "cucumber";
switch (veggie) {
case "cabbage" -> {
System.out.println("A cabbage");
}
case "brussel sprout" -> {
System.out.println("A brussel sprout");
}
case "cucumber" -> {
System.out.println("A cucumber");
}
default -> {
System.out.println("Other");
}
}
}
ints
You can also use int
s with switches.
void main() {
int year = 2024;
switch (year) {
case 2023 -> {
System.out.println("The Chiefs");
}
case 2024 -> {
System.out.println("The Chiefs");
}
default -> {
System.out.println("I don't know");
}
}
}
Enums
Switches really shine with enums.
For each case label you need to use the name of the variant, not prefixed with the name of the enum.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight light = StopLight.GREEN;
switch (light) {
case RED -> {
System.out.println("Stop!");
}
case YELLOW -> {
System.out.println("Speed up, coward!");
}
case GREEN -> {
System.out.println("Go!");
}
}
}
Omitted Default
If you have no logic to put in it, you can omit the default
case from a switch
. This is the same conceptually as omitting a final else
from a chain of if
s and else if
s.
void react(String fruit) {
switch (fruit) {
case "apple" -> {
System.out.println("WOW");
}
case "orange" -> {
System.out.println("Zoinks!");
}
case "grape" -> {
System.out.println("Zoopers!");
}
}
}
void main() {
react("passionfruit"); // 🤷
}
Exhaustiveness
Switches are considered exhaustive if they have a case label for every possible value of the type of thing they are switching over.
This is important if you try to return from a function within a switch. Since you need to return a value from every possible branch the function may take, you either need to add an extra return after the switch expression or have an exhaustive switch.
String describe(int number) {
switch (number) {
case 1 -> {
return "loneliest";
}
case 2 -> {
return "loneliest since 1";
}
}
// Since no default, need a return here
return "Its a number";
}
When you have something like an enum you don't need a default
case
because you can handle every variant explicitly.
enum Bird {
TURKEY,
EAGLE,
WOODPECKER
}
boolean isScary(Bird bird) {
switch (bird) {
case TURKEY -> {
return true;
}
case EAGLE -> {
return true;
}
case WOODPECKER -> {
return false;
}
}
}
Combining Cases
If you have multiple constant values that should be handled in the same way, you can combine case labels by separating their values with a comma.
String scientificName(String vegetable) {
switch (vegetable) {
case "apple" -> {
return "Malus pumila";
}
case "cabbage", "brussel sprouts", "kale", "cauliflower" -> {
// Look it up. Kinda wild.
return "Brassica oleracea";
}
default -> {
return "unknown";
}
}
}
void main() {
System.out.println(scientificName("cabbage"));
}
null
When a switch is given a null
value a NullPointerException
will be thrown immediately.
void eat(String food) {
switch (food) {
case "dog food" -> {
System.out.println("Crunch");
}
case "cat food" -> {
System.out.println("Slorp");
}
default -> {
System.out.println("Other food");
}
}
}
void main() {
eat(null);
}
The only exception to this is when a switch has an explcit case null
in its list of
case labels. default
will not suffice.
void eat(String food) {
switch (food) {
case "dog food" -> {
System.out.println("Crunch");
}
case "cat food" -> {
System.out.println("Slorp");
}
case null -> {
System.out.println("No food");
}
default -> {
System.out.println("Other food");
}
}
}
void main() {
eat(null);
}
A default branch and a null case can be combined by separating them with a comma.
void eat(String food) {
switch (food) {
case "dog food" -> {
System.out.println("Crunch");
}
case "cat food" -> {
System.out.println("Slorp");
}
case null, default -> {
System.out.println("Other food");
}
}
}
void main() {
eat(null);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named isSorcerer
. If the method is given
any of "yuji"
or "gojo"
return true
. Otherwise return false
.
Start by writing this with if
and else
. Then alter the code so it uses
switch
instead.
boolean isSorcerer(String name) {
// CODE HERE
}
void main() {
System.out.println(
isSorcerer("yuji")
);
System.out.println(
isSorcerer("gojo")
);
System.out.println(
isSorcerer("yugi") // Wrong series
);
}
Challenge 2.
Same basic challenge as above, but write a method named didRedSoxWin
which takes an int
representing a year.
Return true
if that is a year the Boston Red Sox won a world series.
false
otherwise. This time start off by using a switch.
boolean didRedSoxWin(int year) {
// CODE HERE
}
void main() {
System.out.println(
"2004: " + didRedSoxWin(2004)
);
System.out.println(
"1998: " + didRedSoxWin(1998)
);
System.out.println(
"2013: " + didRedSoxWin(2013)
);
System.out.println(
"1903: " + didRedSoxWin(1903)
);
}
Challenge 3.
Make a method named transition
which takes in a StopLight
and returns the next light it will transition to.
For those who don't drive cars: red lights go to green, green lights go to yellow, and yellow lights go to red.
This is a duplicate of a challenge from a previous section. This time
do it using a switch
.
enum StopLight {
RED,
YELLOW,
GREEN
}
StopLight transition(StopLight current) {
// ------------
// CODE HERE
// ------------
}
void main() {
var light = StopLight.RED;
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
}
Challenge 4.
In reality a StopLight
can also be broken and not function
at all. Alter transition
so it accounts for a new BROKEN
state which transitions to itself. (BROKEN
goes to BROKEN
).
enum StopLight {
RED,
YELLOW,
GREEN,
BROKEN
}
StopLight transition(StopLight current) {
// ------------
// CODE HERE
// ------------
}
void main() {
var light = StopLight.RED;
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
light = transition(light);
System.out.println(light);
}
Challenge 5.
Given a type of bear, return the correct course of action if you run into one in the wild and it attacks you.
If the bear is null
, return null
as the action to take.
If you don't know what to do when you run into a bear, look it up. Use switch
first then try writing the same logic using if
and else
.
enum Bear {
POLAR,
BROWN,
BLACK,
PANDA,
KOALA
}
enum Action {
LAY_DOWN,
FIGHT_BACK,
RUN_AWAY
YEET
}
Action inCaseOfBearAttack(Bear bear) {
// CODE HERE
}
void main() {
System.out.println(
inCaseOfBearAttack(Bear.POLAR)
);
System.out.println(
inCaseOfBearAttack(Bear.BROWN)
);
System.out.println(
inCaseOfBearAttack(Bear.BLACK)
);
System.out.println(
inCaseOfBearAttack(Bear.PANDA)
);
System.out.println(
inCaseOfBearAttack(Bear.KOALA)
);
System.out.println(
inCaseOfBearAttack(null)
);
}
Constructors
When defining a class, you are allowed to make a special kind of method called a constructor.
A constructor runs before any code gets access to an instance of the class. You use it to set up the "initial state" for an object.
Declaration
To declare a constructor you make a method where you don't write any return type and whose name is the name of the class.
class Muppet {
Muppet() {
}
}
Inside of the constructor's body, you can set the initial values for any fields in the class.
class Muppet {
boolean talented;
Muppet() {
talented = true;
}
}
void main() {
Muppet gonzo = new Muppet();
System.out.println(gonzo.talented);
}
The Default Constructor
If you don't declare a constructor, it will be the same as declaring an "empty" constructor. All fields will be initialized to their default values.
class Muppet {
String name;
boolean talented;
Muppet() {
}
}
void main() {
Muppet gonzo = new Muppet();
// null
System.out.println(gonzo.name);
// false
System.out.println(gonzo.talented);
}
Arguments
If you declare a constructor which takes arguments, you can use those arguments to give initial values to fields.1
You need to pass the arguments in the ()
in the new
expression, just like any other method invocation.
class Muppet {
String name;
Muppet(String name) {
this.name = name;
}
}
void main() {
Muppet gonzo = new Muppet("Gonzo");
// "Gonzo"
System.out.println(gonzo.name);
}
When you declare a constructor that takes arguments, the default constructor will no longer be available.
class Muppet {
String name;
Muppet(String name) {
this.name = name;
}
}
void main() {
// Need to provide a name now
Muppet gonzo = new Muppet();
}
Using this.
for disambiguation comes in handy here, since often the names of arguments
will be the same as the fields you want to populate with them.
Final Fields
If you declare a field as final
, its value cannot be changed after an instance of a class is made.
You are required to explicitly initialize final
fields in the constructor.
class Muppet {
final String name;
Muppet(String name) {
// Without this, it wouldn't work
this.name = name;
}
}
void main() {
Muppet gonzo = new Muppet("Gonzo");
System.out.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();
System.out.println(gonzo.talented);
}
This is primarily useful for "constant" values. You will need these, but having constants attached to instances is a bit unique and won't happen that often.
Invariants
Just like any other method, constructors can throw exceptions.
You can use this fact to establish what we call an "invariant."
Say we have a final
age field and that the constructor for a class throws an exception
if a given age is negative.
class Muppet {
final String name;
final int age;
Muppet(String name, int age) {
this.name = name;
if (age < 0) {
throw new RuntimeException("Age cannot be negative");
}
this.age = age;
}
}
void main() {
Muppet bigBird = new Muppet("Big Bird", 6);
System.out.println(
bigBird.name + " is " + bigBird.age + " years old."
);
}
In every other part of our program now we can rely on age being a non-negative number. That is a property of instances that will not change.1
This is a lot more useful than it seems at first, stay tuned.
It will not change. It will not vary, it is in-variant. Get it?
Overloads
Just like normal methods, you can have multiple constructors so long as each constructor takes different types or different numbers of arguments.
class Muppet {
String name;
boolean talented;
Muppet(String name) {
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
this.name = name;
this.talented = talented;
}
}
When you call a constructor, Java will know what code to run because it knows the types of and number of arguments you are passing.
class Muppet {
String name;
boolean talented;
Muppet(String name) {
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
this.name = name;
this.talented = talented;
}
}
void announce(Muppet muppet) {
System.out.print(muppet.name);
if (muppet.talented) {
System.out.print(" is ");
}
else {
System.out.print(" is not ");
}
System.out.println("talented.");
}
void main() {
Muppet fozzie = new Muppet("Fozzie");
announce(fozzie);
// Always a critic...
Muppet waldorf = new Muppet("Waldorf", false);
announce(waldorf);
}
Delegation
It is common for overloaded constructors to be "shortcuts" for eachother.
That is, if one overload takes two arguments another will take just one argument and do the same logic as the first but fill in a default value for the un-provided value.
class Muppet {
final String name;
final boolean talented;
Muppet(String name) {
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
this.name = name;
this.talented = talented;
}
}
A downside of this is that any validation logic done in one constructor needs to be copy pasted to the other.
class Muppet {
final String name;
final boolean talented;
Muppet(String name) {
if (name.length() == 0) {
throw new RuntimeException("Cannot have blank name");
}
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
if (name.length() == 0) {
throw new RuntimeException("Cannot have blank name");
}
this.name = name;
this.talented = talented;
}
}
To avoid this situation, you can have one constructor "delegate" to another.
To do this you write this
in one constructor and call it as if it were a method.
This will run the logic of the constructor which matches the values passed in.
class Muppet {
final String name;
final boolean talented;
Muppet(String name) {
// Will use the other constructor, but with false filled in
// as a default value
this(name, false);
}
Muppet(String name, boolean talented) {
// This logic now only needs to live in one place.
if (name.length() == 0) {
throw new RuntimeException("Cannot have blank name");
}
this.name = name;
this.talented = talented;
}
}
Challenges
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a Shoe
class. Give each shoe a name
field and a quality
field.
Have these fields be initialized inside of a constructor.
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE);
System.out.println(
"SHOE: " + nike.name + ", " + nike.quality
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
System.out.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality
);
}
Challenge 2.
Add a new price
field to the Shoe
class you wrote above.
Add a new constructor which accepts a third argument to set the price
.
Keep the old two argument constructor around as well. When that one is
used set price
to null
.
Hint: Use Double
to represent a nullable price.
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe jays = new Shoe("Air Jordans", Quality.FINE, 130.0);
System.out.println(
"SHOE: " + jays.name + ", " + jays.quality + ", $" + jays.price
);
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE, 25);
System.out.println(
"SHOE: " + nike.name + ", " + nike.quality + ", $" + jays.price
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
System.out.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality + ", $" + jays.price
);
}
Challenge 3.
Alter the Shoe
class so that the price
field is final.
Alter its constructors so that if they are given a negative
value they throw an exception instead of finishing normally.
Keep in mind that while null
is allowed (you might not know the price)
a negative number wouldn't be. Nobody is paying you to take their shoes.1
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe jays = new Shoe("Air Jordans", Quality.FINE, 130.0);
System.out.println(
"SHOE: " + jays.name + ", " + jays.quality + ", $" + jays.price
);
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE, 25);
System.out.println(
"SHOE: " + nike.name + ", " + nike.quality + ", $" + jays.price
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
System.out.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality + ", $" + jays.price
);
Shoe shouldCrash = new Shoe("Base Ball Cleats", Quality.SUPA_FINE, -10);
}
If they do, you are going to dig holes in the desert in search of treasure that rightfully belongs to you.
Challenge 4.
If you haven't yet, rewrite your Shoe
constructors so only one of them actually sets fields
and the other just delegates to that one.
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe jays = new Shoe("Air Jordans", Quality.FINE, 130.0);
System.out.println(
"SHOE: " + jays.name + ", " + jays.quality + ", $" + jays.price
);
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE, 25);
System.out.println(
"SHOE: " + nike.name + ", " + nike.quality + ", $" + jays.price
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
System.out.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality + ", $" + jays.price
);
Shoe shouldCrash = new Shoe("Base Ball Cleats", Quality.SUPA_FINE, -10);
}
Global Fields
You can make field declarations outside of a class. This makes these fields "global" to the program.
We call things global when they are available at every point in the "world" of our program.1
int number = 0;
void main() {
System.out.println(number);
number++;
System.out.println(number);
}
This explanation is I think a correct description of what it means to be global, but what I am showing won't really hold up as a global thing the more you learn.
Default Values
Just like regular fields in classes, global fields will initialize to their default value unless explicitly initialized.
int x;
int y = 5;
void main() {
System.out.println(x);
System.out.println(y);
}
Final Fields
And just like regular final fields, you always need to give an initial value to global final fields.
final int x = 0;
void main() {
System.out.println(x);
}
Final fields of course cannot be changed.
Field Access
You can access global fields by writing their name. This works from within top level methods as well as instance methods of classes.1
final String monster = "Dracula";
class Mash {
void itWasThe() {
System.out.println(monster + " and his son");
}
}
void main() {
System.out.println(monster + " was there");
Mash mash = new Mash();
mash.itWasThe();
}
This is so convenient it is actually going to be a bummer once you learn what is going on here and can't do it for most of your programs anymore.
Inferred Types
Just like other field declarations, var
cannot be used
for inferring the type of a global field. You need to explicitly write out the type.
var x = 5;
void main() {
System.out.println(x);
}
Challenges
Code is Read more than Written
Now that you more or less can write a full program, it won't be long before you write a program of "signficant size."
This makes it a good time to start drip feeding some of the higher level concepts around software development.
The first of these is a statement: Code is read more then it is written.
Meaning
While I'm sure there are some statistics behind this1, its generally accepted that the majority of a programmer's time is taken up reading existing code rather than writing new code.
This should make sense intuitively. If you get hired at Microsoft, chances are you won't be given a blank slate to make a brand new thing. Often you will be thrust into existing codebases. To make a change to an existing codebase, you need to understand the exisiting code.
It gets to the point where you might spend a whole day reading a dozen or so files only to make a 6 line change to one of them.
Software Development is still a relatively new field. Some things we take for granted may end up not being true after all. It doesn't help that its also pretty poorly researched. Keep an eye out for that.
Implications
This means that, sometimes, it makes sense to write code in a way that is "harder" than is absolutely needed.
That's a bit of a fuzzy statement, but on the more obvious side it means doing things like spending extra time on code formatting, variable naming, function contracts, writing comments, etc.
All of those things make it easier to read that code later on.
Information Density
An important related concept is information density.
Yes, you can do things like add a comment on every line of code. This does mean that a later reader will have context on exactly what you were thinking as you were writing the code.1
The downside is that there can be way more "extra" information than any educated person needs to understand the code. That extra information takes up space and spreads out the "important" information.
Put another way, if something has a high information density it can be hard to read.
M.M.A.T.B.C
Same as if it is has too low an information density.
Dearest sir, it would behoove you to meetest mineself posthaste after the school hours. I would perchance suggest that the location be betwixt the basketball courts.
You want an information density that is "just right"
Meet me at the basketball court
There is a whole sect of folks who write their programs as actual books. Look up "Literate Programming" to dive deeper in to that.
Audience
Consider this opening from an astrophysics paper I picked at random.1
The dearth of planets with sizes around 1.8 R⊕ is a key demographic feature discovered by the Kepler mission. Two theories have emerged as potential explanations for this valley: photoevaporation and core-powered mass-loss. However, Rogers et al. (2021) shows that differentiating between the two theories is possible using the three-dimensional parameter space of planet radius, incident flux, and stellar mass.
If you aren't an astrophysicist, this probably requires some explanation. If you are an astrophysicist, you probably understood that without issue.
Whenever anyone writes anything, it is important to consider the audience you are writing to. This applies to code just as much as to any other form of writing.
This is why shorthands like for (int i = 0; i < length; i++)
are generally considered okay even though
in most other situations i
is pretty non-descript. Within the audience of programmers it is a known idiom.
Its also why explaining every single line of code with comments is generally not okay. Programmers know what code is, you don't need to baby them through how loops work.2
https://arxiv.org/abs/2302.00009
No offense intended if you are still having trouble with loops. Its common, you will eventually get past it.
Practice
If you want to do programming as your job or as part of your job, and you want to be competent, it isn't enough to just practice writing code. You also need to practice reading.
There are a lot of ways to go about that, but an easy one is to read the code of your classmates or of strangers on the internet.
Read their code, try to understand all the (maybe wacky!) choices they made while writing it, and try to change it up a little bit.
This will help you get an intuitive sense for what you like in a codebase as well as what you do not. It will also, more so than the early struggles you go through when learning to write, help you understand if you would actually like to do this for work.
Standard Input
Programs are pretty boring if they just make a machine warm and do some math.
There are a lot of ways to make a program interactive, but the easiest is to read from what is called "standard input."
This function will be added to Java in a future version, but until then copy paste this at the very top of all the code in this section.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
input
To prompt a user for information you use the input
function.
The input
function takes a String
to output as a prompt. This will
work the same as if the String
was passed to System.out.print
.
The program will then wait until a human types some text and clicks the enter key.
Whatever they typed will be returned to the program as a String
.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
String name = input("What is your name? ");
System.out.println("Hello, " + name);
}
Interpreting Input
When you call input
the human on the other side can type whatever they want.
This means that, depending on the question you asked, you might need to interpret
what they typed as something other than a generic String
.
In its most basic form this will look like seeing if one String
equals another String
.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
while (true) {
String shouldExit = input("Exit the program? (y/n)");
if (shouldExit.equals("y")) {
break;
}
}
}
Reprompting
If you ask someone a yes or no question and they respond with "huh?" you might want to ask them again.
This is a good use case for loops. You ask a question and, if the answer you get is acceptable, you proceed as normal. If it is not then you loop back and ask again.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
while (true) {
String response = input("Answer me: yes or no");
if (response.equals("yes")) {
System.out.println("okay then!");
}
else if (response.equals("no")) {
System.out.println("also fine!");
}
else {
System.out.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;
}
}
Leniency
It can make sense to be lenient with your users when interpreting their input.1
This means accounting for common mistakes people make like having extra spaces or capitalizing things incorrectly.
For this purpose, methods like strip
and equalsIgnoreCase
are useful.
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
while (true) {
String response = input("Answer me: yes or no").strip();
if (response.equalsIgnoreCase("yes")) {
System.out.println("aight");
}
else if (response.equalsIgnoreCase("no")) {
System.out.println("cool");
}
else {
System.out.println("try again");
continue;
}
break;
}
}
People are idiots. Their fingers are fat and their wills are weak.
Delayed Assignment
When you have variables declared inside of that loop cannot be seen from the outside. This poses a problem when you are asking someone a question in a loop but want their response to be visible later on in the program.
One strategy for this is to declare any response-holding variables outside the loop.
The problem with this is that Java isn't smart enough to know that you always initialize those variables.
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
String name;
while (true) {
String name = input("What is your name? ");
if (name.isBlank()) {
System.out.println("Name cannot be blank!");
continue;
}
break;
}
System.out.println("Hello " + name);
}
To get around this you can either give an explicit default value.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
String name = null;
while (true) {
String name = input("What is your name? ");
if (name.isBlank()) {
System.out.println("Name cannot be blank!");
continue;
}
break;
}
System.out.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.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
String name;
do {
String name = input("What is your name? ");
if (name.isBlank()) {
System.out.println("Name cannot be blank!");
continue;
}
break;
} while (true);
System.out.println("Hello " + name);
}
Transporting Data
If you ask someone multiple questions you likely will get multiple variables worth of information.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
String firstName = input("What is your first name? ");
String lastName = input("What is your last name? ");
System.out.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.
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
void main() {
String firstName;
do {
firstName = input("What is your first name? ");
if (firstName.isBlank()) {
System.out.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
String lastName;
do {
lastName = input("What is your first name? ");
if (lastName.isBlank()) {
System.out.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
System.out.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
import java.util.Scanner;
Scanner scanner = new Scanner(System.in);
String input(String message) {
System.out.print(message);
return scanner.nextLine();
}
class Person {
String firstName;
String lastName;
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Person askForName() {
String firstName;
do {
firstName = input("What is your first name? ");
if (firstName.isBlank()) {
System.out.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
String lastName;
do {
lastName = input("What is your first name? ");
if (lastName.isBlank()) {
System.out.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
return new Person(firstName, lastName);
}
void main() {
Person person = askForName();
System.out.println("Hello " + person.firstName + " " + person.lastName + ".");
}
When you make a class just to make objects which transfer data between different parts of your program we call those DTOs - data transfer objects. You will learn better ways to make DTOs in the future.
Hardware
That was a long stretch of Java, so I think you've earned a bit of a detour into general computing knowledge.
I won't be offended if you skim or skip these sections. Depending on who you are, this might be a bit obvious.
First lets cover hardware. The actual physical components that make up your computer.
CPU
At the core of every computer is a CPU.
This stands for "central processing unit." This is the rock that wizards taught how to think.
If you send the right electrical signals into a CPU it can do all sorts of math and it can do it quickly.
Inside a computer this looks like a little metal square.
RAM
CPUs can do math real fast, but they can't remember much stuff.1
This is what RAM is for. RAM stands for "random access memory." The random access part just means that a CPU can ask for any bit of info at any time.
Most computers nowadays have an even number of RAM sticks. This is for interesting reasons I don't fully understand.
In 8th grade I had someone physically hand me a piece of paper with their name and fun facts about them because I had kept forgetting their name the entire year. I don't know what Sam is up to now, but I remember that she likes pistachio ice cream.
Hard Drives
RAM sticks have a few issues in the memory department.
First, they can only hold around 8-64 gigabytes of info. Any more than that and they can get pretty slow.1
Second, when you turn off the computer they lose anything they are storing. This is a problem when you are writing an essay and need to turn off the computer half way through.
Hard drives are like RAM in that they remember stuff. The difference is that hard drives can store a lot more of it (terrabytes) and the data isn't lost when power is turned off.
The key word here is "latency". When the CPU needs info in RAM it needs to wait for RAM to find it. It takes longer to find something in a bigger pile of stuff, no matter how smart you are about it.
Motherboard
All of these parts, and the parts I didn't mention, are connected to a big circuit board called a "motherboard."
The motherboard connects the CPU, RAM, and any hard drive(s) such that they can "talk" to eachother. The details of how it does this aren't super important, so just imagine that a tiny wizard lives inside it.
Operating Systems
The exact electrical signals you need to send to make a computer "work" are, obviously, a bit complicated.
This is where operating systems come in. Operating systems are programs that "operate" all the parts that make up your computer.
Personal Computers
For "personal computers", which I am defining as the sort of computers that have a keyboard and a screen, the most common operating system is Windows. The second most common is Mac OS.
These are both produced by massive corporations in order to sell pre-installed on computers. As such, they give you a graphical interface that you navigate with a mouse/touchpad and keyboard. This is what people have found comfortable and what most are used to.
To write code and do other programmer things you have to install programs but once you know what you need to install its pretty easy.
Servers
For servers, the kind of machines you run in a datacenter to host websites, the most common operating system is Linux.
Because so many jobs are writing websites to host on servers, you will eventually need to learn how to work with Linux.
That will be a fun adventure. An adventure I implore you not to explore by bricking1 your family's or a school's computer trying to install Linux on it. Only mess with your own stuff.
I.E. making useless
Mobile Phones
Android and iOS are the major players in the mobile device world.
You will find doing the tasks a programmer needs to exceedingly difficult on these. This is because the interfaces made by these operating systems are designed for use by the general public on small devices. Working with a keyboard and editing a Java program aren't tasks that are prioritized.
Game Consoles
Game Consoles generally always do their own wacky thing.
For the same reasons as mobile devices, you will have a lot of trouble trying to program on a game console. They are special made to run Halo.1
Yeah, Halo sucks now. Halo 3 was art. Obtuse, beautiful, orchestral art. I am old enough to have lived in the age of dreams and to know of their dimming.
Abstractions
The most important jobs of an operating system is to "abstract" over the hardware.
You shouldn't need to know what brand network card you have in order to write code that connects to the internet. You shouldn't need to know what kind of hard drive will be on the machine to store data.
We call these abstractions because they take things that are concrete, like a dozen specific models of hard drive and the exact bits to send to do their operations, and make an "abstract model" of their commonalities.1
This lets programmers write programs that will run on any machine that has the right operating system. It doesn't help writing programs that will run on any operating system because different operating systems provide different abstractions over the same hardware. The code to write to a hard drive in Windows is different than it is in Mac OS.
There are deeper and shallower versions of this explanation which we will get to. There will be plenty of time to talk about this concept as it relates to Java and other things.
Defaults
A social aspect of operating systems is that they control the "default" experience people have with computers.
Installing an operating system is prohibitvely difficult, so computer manufacturers install one before selling a device. When they choose Windows and Windows comes with Internet Explorer built in, people use Internet Explorer.1
There are a lot of kinda grim things that follow from this, and you should dig deeper, but I bring it up to mention the 1980s.
The personal computers available then, like the Commodore 64, only had text based interfaces. In many practical ways, people were closer to the world of programming.2
So think about that whenever you feel like you have a lot left to learn. A lot of what you do and do not know about computers was dictated for you by the fact that you grew up interacting with them on a touch screen instead of on a terminal. Things are not as intimiating as they seem.
https://en.wikipedia.org/wiki/United_States_v._Microsoft_Corp.
Ever wonder why the kid in WarGames was a hacking wizz? The kids in basically every 80s movie? And yet all most of us can pull off is opening Google.
The Terminal
Early computers didn't have graphical interfaces with windows or buttons you could "click." Instead, they offered a text based interface.
We call this sort of interface a "terminal," though you might hear the terms "shell" and "terminal emulator" used interchangibly.
Bash
If you are programming on a computer running either Mac or Linux you should have easy access to a bash terminal. Just search your applications for something called "terminal" or that has the word "term" in it.
This is the most common kind of terminal for working professionals to use.1
Asterisks apply, of course. I didn't run or try to find a survey so this is mostly anecdotal.
Windows Subsystem for Linux
If you are using Windows there are two terminals that come preinstalled. One is cmd.exe
, where
you write in a language called batch, and the other is "PowerShell."
Both of these differ in significant ways from bash so, if at all possible, you should get set up with the Windows Subsystem for Linux.
This will let you follow along with the bash snippets you'll see later in this book.1
It is certainly possible for me to also include instruction for PowerShell and batch but it doesn't feel practical. I spend most of my working hours using batch and can test commands on the machine I use to write this. It would be hard for me to do that with the Windows specific shells
Chromebooks and School Computers
A very common thing that school systems will do is prevent students from accessing a terminal.
This is a good thing if your goal is to prevent high schoolers from committing shenanigans. Less so if your goal is to learn how to program.
There are also computer types, like chromebooks, which aren't really built with programming in mind. While you could access a terminal, the sorts of programs you would generally install to work with Java will be out of reach. It's a similar case for things like iPads.
If you are in one of these situations I would reccomend getting a "normal" computer if at all possible. You can make do for a time, but eventually it will be required to continue in this field.1
Thems the ropes, kid. 🤷
Commands
The primary way you interact with a terminal is by running commands.
You start with a blank prompt and write something like the following.
$ echo Hello
Hello
The first word is the name of the program to run. In this case echo
. Everything after
that is an "argument" to the program.
Directories
Directories, also known as folders, are the places where files are in a computer.
When you open a terminal, you are "inside" a particular directory.
You can see the directory you are in by running the pwd
1 command.
$ pwd
/Users/emccue
"Print Working Directory"
Listing Files
You can list the files in your current working directory with the ls
1 command.
$ ls
Applications
Camera
Desktop
Development
Documents
Downloads
This is useful for getting your bearings, but also to find a file you forget the name of or similar.
Short for "List"
Creating Directories
In order to create a directory you can use the mkdir
1 command.
$ mkdir project
$ ls
project
Make Directory.
Changing Directories
You can change what your working directory is using the cd
command.
After the name of the command, you write the name of the directory you want to change in to.
$ pwd
/Users/emccue
$ cd Downloads
$ pwd
/Users/emccue/Downloads
If you want to go to a "parent" directory, you can write ..
. This means "one directory up".
$ pwd
/Users/emccue
$ cd ..
$ pwd
/Users
Short for "change directory"
Creating Files
To create a blank file you can use the touch
1 command.
$ touch hello.txt
This is situationally useful, but more often than not you will create files using a program called a "text editor."
There are some text editors, like vim
, which run entirely
inside the terminal.2
One of the more popular ones at time of writing is Visual Studio Code. If you weren't otherwise shown a different option, that is a decent default. Install it and then you can use it to create and edit files.
I have no clue why this is called touch
.
2: These have their die hard supporters. The lunatics.
3: This book has been written inside Visual Studio Code.
Run Java Programs
If you've properly set up Java on your machine you should be able to use the java
command
to run any code on your machine.
Just write java
followed by the path to a file containing Java code.1
Say we had this code in a file named Main.java
void main() {
System.out.println("Hello");
}
To run this, say java Main.java
.
$ java Main.java
Hello
If the file is in a directory that is not your current working directory, you should include the full path.
$ java src/Main.java
Hello
You can get away with just using your editor's run button for awhile, but this will eventually become important to know.
Getting Used to it
So far this is only a shallow dive. It will take a good amount of time for you to get comfortable using the terminal for things.
Thats normal. You'll get there eventually. Just know that at least some familiarity with the terminal is going to be needed.
We'll get back to it relatively soon, but feel free to seek out some bash/terminal specific resources online in the meantime.
Exceptions II
Perhaps unsurprisingly, RuntimeException
is not the only kind of exception
that can be thrown.
There is a huge variety of exception types out there, but the most important thing to understand is the distinction between checked and unchecked exceptions.
Checked Exceptions
When a part of your code might throw a "checked" exception, other parts of your code need to account for that possibility.
int divide(int x, int y) {
if (y == 0) {
// Will not work because nothing handles the exception
throw new Exception();
}
return x / y;
}
void main() {
}
We call them "checked" because you need to "check" for them.
These are generally1 used when you expect calling code to be able to do something intelligent to recover if the exception is thrown.
This rule is merely a suggestion and people's definitions of "something intelligent" and "recover" vary wildly. Expect some things to throw checked exceptions and others to not and just know that you need to check for the checked ones.
Unchecked Exceptions
When a part of your code might throw a "unchecked" exception, other parts of your code do not need to account for that possibility.
int divide(int x, int y) {
if (y == 0) {
// Will work because you are not forced to handle
// an unchecked exception
throw new RuntimeException();
}
return x / y;
}
void main() {
}
We call them unchecked because you don't need to check for them.
throws
One way to handle a checked exception is to add a throws
to the end
of your method declaration.
int divide(int x, int y) throws Exception {
if (y == 0) {
throw new Exception();
}
return x / y;
}
void main() {
}
This will make it so that calling code needs to check for the possibility of that exception being thrown.
int divide(int x, int y) throws Exception {
if (y == 0) {
throw new Exception();
}
return x / y;
}
void doStuff() {
divide(1, 0);
}
void main() {
}
You can also declare unchecked exceptions using throws
but you are never required to.1
int divide(int x, int y) throws RuntimeException {
if (y == 0) {
throw new RuntimeException();
}
return x / y;
}
void main() {
}
This is one of many things that you might choose to do for the benefit of a human reader that isn't strictly needed for correct Java.
Propagating Exceptions
Say we started out with code like this.
void dream() {
System.out.println("Shin Godzilla's Jaw unhinging like a snake...")
}
void sleep() {
dream();
}
void main() {
sleep();
}
If a function is declared to throw an exception, that exception will have to "propagate" - meaning spread - to all calling functions.
void dream() throws Exception {
throw new Exception("Something went wrong")
}
void sleep() throws Exception {
dream();
}
void main() throws Exception {
sleep();
}
dream
declares that it might throw Exception
so sleep
must declare that it might throw Exception
. Because sleep
might throw Exception
now main
might throw Exception
.
Rethrowing Exceptions
When you catch an exception you have the option of again throwing an exception.
This is useful if you want to have behavior that occurs when something goes wrong but still ultimately throw an exception for the rest of the code to deal with.1
void dream() throws Exception {
throw new Exception("Oh no");
}
void sleep() throws Exception {
try {
dream();
}
catch (Exception e) {
System.out.println("Something went wrong while dreaming");
throw e;
}
}
It is also an opportunity to "wrap" checked exceptions into unchecked ones.
void dream() throws Exception {
throw new Exception("Oh no");
}
void sleep() {
try {
dream();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
void main() {
sleep();
}
Which seems crazy, but remember that "the point" of a checked exception is to have the calling code make a choice about how to handle an error condition. If you are okay with just crashing, throwing an unchecked exception is a perfectly valid choice.
These examples are all silly, I know. Once we get to files the mechanics will become useful.
Exception
The most vanilla1 checked exception is Exception
.
If you want a checked exception and do not know of a better one, Exception
will do.
void main() throws Exception {
throw new Exception("Crash!");
}
people have the audacity to equate vanilla with “plain”. the fruit of a delicate orchid pollinated by hand. worth its weight in solid gold and beyond. the fussy black-and-cream jewel of the american continent. you sick son of a bitch. imagine a world without vanilla. no blondies. no pound cakes. no crème brûlée, no coke floats. no cream soda. no satiny new york-style cheesecakes. no warm apple pie à la mode. no velvety complexity to bring out complex notes in chocolate desserts. no depth of flavour in your cakes and cookies and milkshakes. all in just a few precious seeds or grams of paste or perfumed teaspoons of liquid black platinum. what you don’t understand could fill the library of alexandria seven times over and then some. you ungrateful bastard i’m going to kill you - Tumblr user "kirbyofthestars"
RuntimeException
main
The main
method can be declared to throw any kind of checked exception.
It is assumed that if an exception makes it all the way there that the way to handle it is crash the program.
void main() throws Exception {
throw new Exception("crash");
}
Switch II
A common thing to do is have a switch
statement which assigns a value
to a variable in each branch.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = null; // Delayed assignment rules are funky here.
switch (light) {
case RED -> {
action = Action.STOP;
}
case YELLOW -> {
action = Action.SLOW_DOWN;
}
case GREEN -> {
action = Action.GO;
}
}
System.out.println(action);
}
For this purpose, you can instead use a switch "as an expression."
Yield
To use a "switch expression" you put the entire switch to the right hand side of an equals sign1 and, instead of assigning to a variable, you "yield
" the value you want to assign.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = switch (light) {
case RED -> {
yield Action.STOP;
}
case YELLOW -> {
yield Action.SLOW_DOWN;
}
case GREEN -> {
yield Action.GO;
}
};
System.out.println(action);
}
yield
is very similar to return
. The difference is that return
will exit the entire method. yield
just decides what the switch evaluates to.
Technically we are talking about an "expression context." Meaning a place where you are allowed to put an expression. The right hand side of an equals sign is one, but there are many others.
Omitted Yield
If a branch of a switch just yields a value but does nothing else interesting you can
omit the yield
along with the surrounding {
and }
.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = switch (light) {
case RED -> Action.STOP;
case YELLOW -> Action.SLOW_DOWN;
case GREEN -> Action.GO;
};
System.out.println(action);
}
Which can be mixed with cases that have explicit yield
s and might do other things.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = switch (light) {
case RED -> Action.STOP;
case YELLOW -> {
System.out.println("Lemon Light!");
yield Action.SLOW_DOWN;
}
case GREEN -> Action.GO;
};
System.out.println(action);
}
Exhaustiveness
When a switch is used as an expression it needs to be exhaustive.
void main() {
String name = "bob";
boolean cool = switch (name) {
case "bob" -> false;
default -> true;
};
System.out.println(cool);
}
If you attempt to make a non-exhaustive switch expression, Java will not let you.
void main() {
String name = "bob";
boolean cool = switch (name) {
case "bob" -> false;
};
System.out.println(cool);
}
Return a Switch
If you choose to, you do not need to assign the result of a switch expression into a variable. You can directly return the result from a method.
enum Bird {
TURKEY,
EAGLE,
WOODPECKER
}
boolean isScary(Bird bird) {
return switch (bird) {
case TURKEY -> true;
case EAGLE -> true;
case WOODPECKER -> false;
};
}
Multi-File Programs
For many reasons, it makes sense to split programs into multiple files.
This is one of those things that is easier to show than tell, so to follow along make a blank folder on your computer.
$ mkdir program
$ cd program
The Sources folder
When people work on "a project" - meaning a program made to accomplish some task - there will often be more than just Java code involved.
To deal with this its common to make a folder to put Java code into. I prefer the name
src
for this.
$ mkdir src
And inside this src
folder we will put the code.1
So you should have this
project/ <- Open this in your text editor
src/ <- Code will go here
There are different ways to layout a project. All are valid. You can call the src
folder STUFF
- it ultimately doesn't matter. This is just another one of those social conventions.
The Main file
Inside your src
folder make a file called Main.java
. This is the file where
we are going to write the "start" of the program.
All the Java code you've written up until now will work if you put it into this file.
void main() {
System.out.println("Hello, world!");
}
So you should now have something that looks like this.
project/
src/
Main.java
To run your program, use the java
command.
$ java src/Main.java
Hello, world!
A Second file
In files that are not Main.java
you can put other code, but
only in the form of a class.
By this I mean, while in Main.java
you are able to write something like this.
void sayHello() {
System.out.println("Hello");
}
void main() {
sayHello();
}
In a file named Ball.java
you need to put all code inside a Ball
class.
class Ball {
// You can write constructors, methods, fields, etc.
final int size;
Ball(int size) {
this.size = size;
}
}
// But you cannot have any "top level" methods or things outside
// of the Ball class
Then from Main.java
you can make an instance of Ball
void main() {
var ball = new Ball(10);
System.out.println("The ball is " + ball.size + "cm across");
}
When you run java src/Main.java
it will find src/Ball.java
and use the code in there.
File names
When you make a file called Ball.java
you should expect to find a class named Ball
inside of it.
This is a general rule. When you split code into multiple files each file should be a class and the name of the class should match the name of the file.
So if I want to write a Position
class
class Position {
int x;
int y;
}
That should go in its own file named Position.java
.
The Anonymous Main Class
Surprise! Everything in Java is actually in a class.
This includes the code in Main.java
.
void main() {
System.out.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 put wrap it with class Main {}
it will continue to work as-is.
class Main {
void main() {
System.out.println("yep.");
}
}
What Java will do is run new Main().main();
to start your program.
Global Fields
Global fields, accordingly, were always a lie.
int number = 0;
void main() {
System.out.println(number);
number++;
System.out.println(number);
}
They are just normal fields in the anonymous main class.
class Main {
int number = 0;
void main() {
System.out.println(number);
number++;
System.out.println(number);
}
}
This means that once you move to programs in multiple files they are no longer global to your program - just the code in the main class.1
Huge bummer, but we will learn how to make actually global things later.
Visibility
When code is all in one file, everything is "visible." This means that if there is a method you are always allowed to call it.
class Main {
void canCallThis() {
System.out.println("of course!")
}
void main() {
canCallThis();
}
}
And if there is a field you can read it, if there is a class you can make an instance of it, etc.
Once we split into multiple files, you are allowed to make things less visible.
Private Methods
If a class has a method that it uses but that you do not want other code to see,
you can mark it private
.
class RiceCooker {
int temperature;
private boolean shouldTurnOff() {
return temperature > 100;
}
void operate() {
if (shouldTurnOff()) {
turnOff();
}
else {
// ...
}
}
}
This makes it so that code in RiceCooker.java
can see and call the shouldTurnOff
method but code in other files cannot.
This is useful if you want to box up some logic but don't want to have to think about what happens if other code calls it.
Private Fields
Similar to private methods, you can also mark a field as private.
class Kaiju {
private int timesLostToGodzilla;
Kaiju() {
this.timesLostToGodzilla = 0;
}
void fightGodzilla() {
this.timesLostToGodzilla++;
}
boolean isLoser() {
return this.timesLostToGodzilla > 0;
}
}
This makes it so that code in other files cannot see or change the field directly.
Invariants
Having private fields and methods is useful when you want to maintain some invariants.
Say you wanted a class that holds an even number.
class EvenNumberHolder {
int value;
EvenNumberHolder(int value) {
if (value % 2 == 1) {
throw new RuntimeException(value + " is not even");
}
this.value = value;
}
}
You can always make the value final
to prevent its value from being changed.
class EvenNumberHolder {
final int value;
EvenNumberHolder(int value) {
if (value % 2 == 1) {
throw new RuntimeException(value + " is not even");
}
this.value = value;
}
}
But if you actually wanted to change its value that isn't enough.
By making the field private you can know that other code has to call methods to access or change the value. That gives you a clean place to "enforce" your invariants.
class EvenNumberHolder {
private int value;
EvenNumberHolder(int value) {
// The constructor explicitly rejects an odd value
if (value % 2 == 1) {
throw new RuntimeException(value + " is not even");
}
this.value = value;
}
// There is no way to get an odd value now - you can only
// change it in steps of two.
int value() {
return this.value;
}
void addTwo() {
this.value += 2;
}
void subtractTwo() {
this.value -= 2;
}
}
Accessors
When a field is hidden that is usually because you want to control how it might be changed.
To access the current value of a private field you need to go through a non-private method. If a method just provides access to a field we call that an "accessor."
class Dog {
private String name;
Dog(String name) {
this.name = name;
}
// The name field is private, but
// you can access it by calling the name method.
String name() {
return this.name;
}
}
class Main {
void main() {
var dog = new Dog("Daisy");
// dog.name won't work because the name field is private
// dog.name() will work because the name method is not
System.out.println(dog.name());
}
}
class Dog {
private String name;
Dog(String name) {
this.name = name;
}
String name() {
return this.name;
}
}
We would also consider things like the length
method on String
s to be "accessors."1
void main() {
String s = "abc";
System.out.println(
// We can't see what fields underly this,
// but we can access the length
s.length()
);
}
Not that the categorization matters much, but socially we expect "accessor"-looking methods to only give you a value and not "do stuff" like increment a number or mess with a file.
Getters and Setters
A very silly thing you are likely to see if you dig around on the internet is classes that look like this.
class Person {
private String name;
private int age;
String getName() {
return this.name;
}
void setName(String name) {
this.name = name;
}
String getAge() {
return this.age;
}
void setAge(String age) {
this.age = age;
}
}
So people make classes with all private fields and then for each field thing
they
have two methods - getThing
and setThing
.
To which you might immediately ask - what is the difference between this and just having non-private fields.
class Person {
String name;
int age;
}
The answer to that is...annoying. We'll get to it, but the short story is that its a bit of a holdover from a very weird period in the 1990s.
I mention it specifically so that you know that there isn't any important information you are missing and you are not crazy.
Static Fields
To have a field be truly global for your program you can mark it as static.
class Count {
static int value;
}
Declaration
To declare a field as static, add the static
keyword to its declaration.
class Count {
static int value;
}
Initialization
By default, static fields will be given the same default initial value as other fields.
So a static int
field will be initialized to zero, a static String
field
will be initialized to null
, etc.
class Main {
static int count;
static String name;
void main() {
System.out.println(count); // 0
System.out.println(name); // null
}
}
If you want to initialize them to a different value you do not do that in a constructor like you would a normal field.
You can give them a value directly with =
.
class Main {
static int count = 5;
static String name = "bob";
void main() {
System.out.println(count); // 5
System.out.println(name); // bob
}
}
Or you can initialize them in a "static block". This looks like the word static
followed by some braces {}
with code in the middle.1
class Main {
static int count;
static String name;
static {
count = 5;
name = "bob";
}
void main() {
System.out.println(count); // 5
System.out.println(name); // bob
}
}
The rules for static blocks are actually crazy complicated. Try not to do anything "interesting" in them.
Usage
To use a static field within the class it is declared you just need to write the name of the field.
class Main {
static int count = 0;
void main() {
System.out.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() {
System.out.println(Main.count);
}
}
Constants
Because static fields are global to the entire program, they are the preferred mechanism for storing "constants."
Constants are things that you don't expect to change, so you would also
mark such fields as final
.
class MathConstants {
static final double PI = 3.14;
}
// Then in other parts of your code you can reference MathConstants.PI
Controversy
Static fields are culturally controversial. Specifically static fields which can change.
class Counter {
static int value = 0;
}
In the example above, any part of the code can change the value at any time by writing to Counter.value
.
This is "fine" in small to mid-sized programs, but once you have a hundred thousand lines it can become difficult to reason about what code changes that field and when.
For this reason1 you will probably get a lot of mean comments if you share code that uses a static field you can change.
Using static fields for constants is less controverial.
class Constants {
static final int DAYS_IN_A_WEEK = 7;
}
Well, in addition to the generally rampant immaturity of programmers.
Naming
For static final fields people generally name them the same way you would an enum - LIKE_THIS
.
class Constants {
static final int DAYS_IN_A_WEEK = 7;
static final int WEEKS_IN_A_YEAR = 52;
}
Because you will get yelled at for using a non-final static field no matter what you do, the rules there are less strict. You can name them in all caps or like a normal variable depending on personal preference.
class Counter {
// The people who would be mad at you for the first
// will probably already be mad at you for the second.
static int value = 0;
static int VALUE = 0;
}
Static Methods
If you want to be able to call a method from anywhere in your program you
can use a static
method.
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
class Main {
void main() {
int result = MyMath.add(1, 2);
System.out.println(result);
}
}
Declaration
Same as static
fields, to mark a method as static
you write the word static
before it.
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
Scope
In the definition of a static method you can use variables like normal and you can reference other static fields and methods.
class ScopeExample {
static final int CAN_ACCESS = 3.14;
static void canCall() {
}
static void doStuff() {
canCall();
System.out.println(ScopeExample.CAN_ACCESS);
}
}
But you cannot access any non-static methods or fields. They are not in scope.
class ScopeExample2 {
final int CANNOT_ACCESS = 3.14;
void cannotCall() {
}
static void doStuff() {
cannotCall();
System.out.println(
CANNOT_ACCESS
);
}
}
Naming
Unlike static fields, which get new socially accepted naming rules, you name static methods the same as any other method.
class Naming {
static void nameLikeNormal() {
}
}
Usage
You use a static method by writing the name of the class where it is
defined followed by .
and the method name.
class StuffDoer {
static void doStuff() {
System.out.println("Doing stuff");
}
}
class StuffDoer {
static void doStuff() {
System.out.println("Doing stuff");
}
}
void main() {
StuffDoer.doStuff();
}
Math
One of the most "obvious" usages for static methods is when doing things that are math or math-like.
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
These sorts of methods have a result computed solely from their inputs, so needing an instance of a class to call them would be silly.
The Math
class that comes with Java has methods that work in this way. Math.max(a, b)
is static
and therefore usable everywhere you want the maximum of two numbers.1
There are way more on there. Take a look.
Factories
Another interesting use of static methods is what we would call a "factory" method.
Say you have a class like the following.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
}
void main() {}
It would be reasonable 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);
System.out.println(p1.x + ", " + p1.y);
System.out.println(p2.x + ", " + p2.y);
System.out.println(p3.x + ", " + p3.y);
}
}
This won't work if you defined Position
inside the anonymous main class. I'll tell you why later.
Growable Arrays
Arrays are fixed size collections of elements. This means when we make an array that is 5 elements big it will always be 5 elements big.
void main() {
int[] numbers = new int[5];
}
Something that turns out to be extremely useful is to have something with most of the properties of an array - such as being able quickly get and set arbitrary elements by index - but that can grow over time.
The rest of this section I will walk you through how we can accomplish that.
Concept
The concept behind a growable array is that we store an array as a field and use it for operations like getting and setting elements.
The extra wrinkle is that when someone wants to "add" an element we make a new, bigger, array and copy all the existing elements to it. Then we put the element you wanted to add at the end.
Simple Implementation
Following the previous description to the letter will get you something like this.
Take some time to read it through.
class GrowableIntArray {
// Store an int[] internally
private int[] data;
GrowableIntArray() {
// Make sure to initialize it correctly
this.data = new int[0];
}
// When someone wants to get an item, get it from the array
int get(int index) {
return this.data[index];
}
// Same deal when someone wants to set an item at an index.
void set(int index, int value) {
this.data[index] = value;
}
// And we need an accessor for the size so that someone
// can loop over the array.
int size() {
return this.data.length;
}
void add(int value) {
// Copy the old array to a new, bigger one
int[] newArray = new int[this.data.length + 1];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
// Then put the new element at the end
newArray[newArray.length - 1] = value;
// And swap the array
this.data = newArray;
}
}
Usage
Using the class we defined would look something like this.
class GrowableIntArray {
// Store an int[] internally
private int[] data;
GrowableIntArray() {
// Make sure to initialize it correctly
this.data = new int[0];
}
// When someone wants to get an item, get it from the array
int get(int index) {
return this.data[index];
}
// Same deal when someone wants to set an item at an index.
void set(int index, int value) {
this.data[index] = value;
}
// And we need an accessor for the size so that someone
// can loop over the array.
int size() {
return this.data.length;
}
void add(int value) {
// Copy the old array to a new, bigger one
int[] newArray = new int[this.data.length + 1];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
// Then put the new element at the end
newArray[newArray.length - 1] = value;
// And swap the array
this.data = newArray;
}
}
class Main {
void main() {
// To start we don't know how many elements there are
var array = new GrowableIntArray();
// Each time we add an element the array "grows"
array.add(1);
array.add(2);
array.add(3);
// And we can loop over it like so
for (int i = 0; i < array.size(); i++) {
System.out.println(array.get(i));
}
}
}
Important to note that while we see it as if it was a growable array, no actual arrays are "growing." We are just faking it.
Performance Problems
While the code as is works, it will not perform well.
Since we copy the underlying array every time someone adds a new element it can be expensive to add a lot of elements.
Pretend we were going to add a hundred elements to the growable array.
class GrowableIntArray {
// Store an int[] internally
private int[] data;
GrowableIntArray() {
// Make sure to initialize it correctly
this.data = new int[0];
}
// When someone wants to get an item, get it from the array
int get(int index) {
return this.data[index];
}
// Same deal when someone wants to set an item at an index.
void set(int index, int value) {
this.data[index] = value;
}
// And we need an accessor for the size so that someone
// can loop over the array.
int size() {
return this.data.length;
}
void add(int value) {
// Copy the old array to a new, bigger one
int[] newArray = new int[this.data.length + 1];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
// Then put the new element at the end
newArray[newArray.length - 1] = value;
// And swap the array
this.data = newArray;
}
}
class Main {
void main() {
var array = new GrowableIntArray();
for (int i = 0; i < 100; i++) {
array.add(i);
}
System.out.println(array.size());
}
}
For the each element we need to make a copy of an array. So when we add the first element we need to make an array 1 element big. The second, copy that 1 element array and make a new 2 element one.
So if you do napkin math on the things that need to happen you get this
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 ...
Which is just a crazy number of copies. It means calling .add
on an already big list will be slow and calling .add
a lot of times to make a big list will be very slow.
Performance Solutions
If the problem with the simple implementation is that we make too many copies, the solution is to make fewer copies.
The easiest way to do this is to "over-allocate." Make our internal array bigger than
we actually need it to be. That way most of the time when you call .add
it will be
fast.
We can't - or at least shouldn't - over allocate right away. If every growable array secretly has enough room for millions of elements that would be silly. Better to over-allocate as we go.
Exactly how much we should make the internal array bigger than we need is more art than science. People have found that doubling the size each time is a pretty good tradeoff1.
This data structure is crazy important. Maybe the most common one used in the world. Java has it built-in and we'll get to that later.
Optimized Implementation
So with that context we can update the simple implementation as follows.
Note the use of IndexOutOfBoundsException
. It is an unchecked exception that
comes with Java specifically for when an index is out of bounds.1
class GrowableIntArray {
private int[] data;
// We need a different variable to store the size
// since the array's size will different than we want.
private int size;
GrowableIntArray() {
this.data = new int[0];
this.size = 0;
}
int get(int index) {
// Now that we manage the size, we need to do
// "bounds checks" ourselves
if (index >= size) {
throw new IndexOutOfBoundsException(index);
}
return this.data[index];
}
void set(int index, int value) {
// For setting as well
if (index >= size) {
throw new IndexOutOfBoundsException(index);
}
this.data[index] = value;
}
int size() {
// We use the size we store rather than the length of the array
return this.size;
}
void add(int value) {
// if we won't have enough room in the array, make a new one.
if (size >= this.data.length - 1) {
// Overallocate by 2x
int newLength = this.data.length * 2;
if (newLength == 0) {
newLength = 2; // Unfortunately 0 * 2 is 0, so account for that
}
int[] newArray = new int[newLength];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
this.data = newArray;
}
// And at this point we know the array is big enough
this.data[size] = value;
this.size++;
}
}
I love obvious and circular statements like this, if you haven't noticed.
Command Line Arguments
When you run a program like so
java src/Main.java
Anything you put to the right of src/Main.java
will be available to your program as a
"command line argument."1
java src/Main.java example
They come from the command line and they are arguments to your program.
Accessing Arguments
To access the command line arguments given to your program you need to change your main method.
Instead of
void main() {
}
Have your main method take a string array as an argument.
void main(String[] args) {
}
This String[]
will hold all the arguments passed.
java src/Main.java Duck Squirrel
class Main {
void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println(
"Hello Mr. " + args[i]
);
}
}
}
// This is a little ugly
void main() {
new Main().main(new String[] { "Duck", "Squirrel" });
}
Conventions
As it is just a String[]
, you can interpret the arguments passed to your
program in any way you choose.
What you will notice, however, from using other tools in your terminal is that there are a few socially accepted conventions.
If your tool takes "options" then you would expect --option-name VALUE
to
be how you specify it. So two dashes followed by the argument name.
If you need a shorter version then you allow for one dash and sometimes a single
letter value -d VALUE
.
And most importantly, if someone types --help
you should give them the
available options. Try java --help
for an example.
Inner Classes
You can declare a class within another class.
class Car {
class Speedometer {
}
}
We call these inner classes.
Type
The type of an inner class, when used as a variable or as a field,
is the name of the containing class followed by a .
and the
name of the inner class.
class Car {
class Speedometer {}
}
So a field containing an instance of Speedometer
would have the type Car.Speedometer
.
Car.Speedometer speedometer = ...;
The exception is if the inner class is referenced within the class that declares it. In that context you just need to write the name of the class;
class Car {
// Car.Speedometer is not required
// (it will work though)
Speedometer speedometer;
class Speedometer {}
}
Instances
To make an instance of an inner class you can use new
to invoke its constructor
like any other class.
class Car {
class Speedometer {
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
The restriction is that an inner class can only be constructed from an instance method of the containing class.
This means that, in the example above, you cannot make an instance of Speedometer
unless you first have an instance of Car
.
class Main {
void main() {
var car = new Car();
var speedometer = car.getSpeedometer();
System.out.println(speedometer);
// But this will not work
// var speedometer = new Car.Speedometer();
}
}
class Car {
class Speedometer {
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
New Operator
If you want to make an instance of an inner class without making a method on the class containing it, you can use the "new operator."
Whereas you make an instance of a regular class by saying
something like new ClassName()
, you can make an instance of an
inner class by using .new
on a variable that holds an instance
of the containing class.
Thats 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();
System.out.println(speedometer);
}
}
Scope
Within an inner class all fields and methods of the instance it was created in are available.
class Car {
int speed = 0;
class Speedometer {
int getSpeed() {
return speed;
}
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
class Car {
int speed = 0;
class Speedometer {
int getSpeed() {
return speed;
}
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
class Main {
void main() {
Car car = new Car();
Car.Speedometer = car.getSpeedometer();
}
}
One mental model for this is that its as if the inner class holds a reference to the one it was created in.
class Car {
int speed = 0;
}
class CarSpeedometer {
private final Car madeBy;
CarSpeedometer(Car madeBy) {
this.madeBy = madeBy;
}
int getSpeed() {
return madeBy.speed;
}
}
Disambiguation
If you are within an inner class and want to use a field from the instance it was created in but that field has the same name as a field in the inner class - like the following.
class Car {
int speed = 0;
class Speedometer {
// Speed is declared here, but it is
// a different field
int speed = 5;
}
}
You can disambiguate between the fields by using the name of the containing class
followed by .this
.
class Car {
int speed = 0;
class Speedometer {
// Speed is declared here, but it is
// a different field
int speed = 5;
void saySpeed() {
System.out.println(speed); // 5
System.out.println(this.speed); // 5
System.out.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() {
System.out.println(speed); // 5
System.out.println(this.speed); // 5
System.out.println(Car.this.speed); // 0
}
}
}
class Main {
void main() {
var car = new Car();
var speedometer = car.new Speedometer();
speedometer.saySpeed();
}
}
The anonymous main class
If you remember when I first showed you classes, you were working inside the anonymous main class.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
}
This means that all the classes you made were, in reality, inner classes.
class Main {
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
}
}
Which is how they would have access to any "global fields" you declared and why static factory methods would not work.
class Main {
class Muppet {
String name;
static Muppet fromName(String name) {
// You cannot make an instance of an inner class
// from within a static method, so this wouldn't work.
Muppet muppet = new Muppet();
muppet.name = name;
return muppet;
}
}
void main() {
Muppet kermit = Muppet.fromName("Kermit The Frog");
}
}
Static Inner Classes
If you mark an inner class as static
then it becomes
much closer to a normal class.
class Car {
static class Speedometer {
}
}
You can make instances of it directly without an instance of the outer class.
Car.Speedometer speedometer = new Car.Speedometer();
And it cannot access fields of the instance it was made in, because it was not made in an instance.
class Car {
int speed; // Speedometer can't magically get this anymore
static class Speedometer {
}
}
I would wager that this is the most common kind of inner class to see in real code, despite requiring more words to define1.
A theme that will start to emerge is that the "best" code sometimes has a few extra modifiers on it and the "default" behavior isn't what you want. Static inner classes are way less magic.
Private Inner Classes
Both static
and regular inner classes can be marked as private
.
class Human {
// No other class can see this human's thoughts
private class Thoughts {
}
// Nor can they see their feelings
private static class Feelings {
}
}
Within the class they are defined, a private inner class works as normal. The difference is that code outside the class cannot make instances of them.
Packages
Every class in Java "lives" in a package.
A package is a way to group individual classes.
package dungeon;
class BugBear {
}
Declaration
To put a class into a package you need to do two things.
First, put a "package declaration" at the top of the file. This looks like the word package
followed by the name of the package and a ;
package dungeon;
class BugBear {
}
Then you need to make sure that the .java
file is in a folder matching the name of
that package.
So, for the example above, if your code was previously laid out like this
src/
BugBear.java
It needs to be changed to this.
src/
dungeon/
BugBear.java
Visibility
By default, a class can only see the other classes in its package.
package dungeon;
class BugBear {
}
package village;
class Villager {
}
package dungeon;
class Dwarf {
BugBear fight() {
// Can "see" BugBear and thus call its constructor,
// access visible fields and methods, etc.
return new BugBear();
}
// Cannot see Villager because it is in a different package.
}
Public Classes
To be able to use a class in one package from a different package,
you must first mark that class as public
.
package village;
// Now other packages will be able to see it
public class Villager {}
Fully Qualified Class Name
In order to use a public class from a different package you can write out the "fully qualified class name."1
This is the name of the class prefixed with the package it is in.
package village;
public class Villager {}
package dungeon;
class Dwarf {
// Works because we write out the full name
// and because Villager is public.
village.Villager meet() {
return new village.Villager();
}
}
This hints that the "real name" of a class isn't what you write after class
, but instead both that and the package name glued together.
People also often call this the "FQCN". Its a fun acronym to write, but I have no clue how to say it out loud. "Faquacün?"
Import
If you don't want to write out the fully qualified class name everywhere, and few do,
then you can use an import
.
To import a class you write import
followed by the fully qualified class name.
This makes it so that the "simple" class name will be usable.
package village;
public class Villager {}
package dungeon;
// For the rest of the file, you can
// just write "Villager" and Java will know
// what you mean.
import village.Villager;
class Dwarf {
Villager meet() {
return new Villager();
}
}
The Default Package
When your classes don't have a package declaration, we say those are in the "default package."
// No package declaration means default package
public class Elf {
}
Classes in the default package cannot be imported by classes in named packages, regardless of if those classes are public.
package villager;
public class Villager {
// No way to reference Elf directly,
// even if Elf is public
}
Because of this restriction1 you will mostly use the default package when you are feeling lazy or are making a smaller program.
And more to come!
The Anonymous Main Class
You are only allowed to make an anonymous main class inside the default package.
// Allowed
void main() {
System.out.println("Hello, world");
}
// Not Allowed
package myprogram;
void main() {
System.out.println("Hello, world");
}
This means that for classes in packages you have to wrap them in an explicitly named class like everything else.
// Allowed
package myprogram;
class Main {
void main() {
System.out.println("Hello, world");
}
}
Public Methods
Even though a class might itself be marked public
, the methods
within it will not be unless they are also public
.
package village;
// Now other packages will be able to see it
public class Villager {
public void isVisible() {
System.out.println("This method is callable from another package.");
}
void isNotVisible() {
System.out.println("This method is not.")
}
}
This applies also to static methods.
package village;
public class Well {
public static int drawWater() {
System.out.println("""
You need this to be both public and static to
be able to write Well.drawWater()
""");
return 235;
}
}
Package-Private Methods
If a method is neither public
or private
then it can
be used only by code in the same package.
We call these methods "package-private" because they are "private" to code outside the package.
package village;
public class Villager {
void isNotVisible() {
System.out.println("""
This method can be called from code in the 'village'
package, but not from other packages.
""");
}
}
Which again applies to static methods as well.
Public Fields
Similarly to methods, for a field to be used from a different package
it must be marked public
.
package village;
public class Well {
// Both of these you can use from a different package
public static final int DEPTH = 10;
public int remainingWater;
}
Package-Private Fields
Fields that are marked neither public
nor private
are "package-private."
package village;
public class Well {
// Neither of these can be used outside of the "village" package
static final int NUMBER_OF_DEMONS = 4;
boolean exorcismPerformed;
}
The Default Constructor
If a class is public
and has a default constructor - i.e. the constructor you
get when you don't specify one - then the default constructor you get will be
public
.
package dungeon;
public class Skeleton {
// No constructor specified
}
package village;
import dungeon.Skeleton;
class Main {
void main() {
// We can say `new Skeleton()` here
var skeleton = new Skeleton();
}
}
If you write out a constructor yourself this will not be the case.
package dungeon;
public class Skeleton {
public final int bones;
Skeleton() {
this.bones = 206;
}
}
package village;
import dungeon.Skeleton;
class Main {
void main() {
// Now "new Skeleton()" will not work.
var skeleton = new Skeleton();
}
}
Public Constructors
For a constructor you write to be usable across packages1 it needs
to be marked public
.
package dungeon;
public class Skeleton {
public final int bones;
public Skeleton() {
this.bones = 206;
}
}
package village;
import dungeon.Skeleton;
class Main {
void main() {
// Now this works
var skeleton = new Skeleton();
// And we get the right number of bones!
System.out.println(skeleton.bones);
}
}
You guessed it!
Package-Private Constructors
A constructor without any other modifier is "package-private" in the same way as methods and fields.1
package dungeon;
public class Slime {
final int size;
// This constructor is public,
// code in other packages can use it.
public Slime() {
this.size = 5;
}
// This constructor is package-private,
// code in other packages cannot use it.
Slime(int size) {
this.size = size;
}
}
Your spider-sense might be tingling wondering if private
constructors
are a thing. They are! I'll talk about them more in-depth later, but they can be
surprisingly useful.
Subpackages
Packages can also have subpackages.
What this looks like is for any package with a .
in its name - say my.fun.project
-
you need a new folder.
So if you have a class like this
package my.fun.project;
public class Apple {}
It should be in a folder structure like this
src/
my/
fun/
project/
Apple.java
And the fully qualified class name would be my.fun.project.Apple
.
Reverse Domain Name Notation
Part of the point of putting classes into packages is to avoid conflicts when using code written by other people.
If you want a class named RiceCooker
and you want to use a code that someone
on the internet wrote which just so happens to also have a class named RiceCooker
,
that only works if you both put your classes in different packages.
Making sure different people cooperate is a hard problem, so the social convention that emerged was to name packages using a "reverse domain name notation."
Only one person could own a domain name - like google.com
- at a time. So all
code coming out of Google1 would start with a package name which is the reverse
of that - com.google
.2
Nowadays people also tend to accept unique prefixes based on accounts you might have on a site. So you might see things like com.github.their_username_here
for people who have an account with a service like Github
.
It isn't perfect, nothing would be, but its socially dominant so you should be aware of it. If the code you are writing won't be shared with others you do not need to do this sort of thing yourself.
This is why many tools will have your starter project have classes in the com.example
package
Records
If you have a class whose only purpose is to ferry data around,
you can instead use a record
.
record Person(String name, int age) {}
Declaration
To declare a record you write the word record
followed by a
class name, a list of "record components" in parentheses, and a pair of {}
.
The list of record components should be reminiscent of function arguments.
record Person(String name, int age) {}
Having an empty list of record components is also allowed.1
record Pants() {}
As to why you might want to give an empty list of record components, its nuanced. For now its just for fun.
The Canonical Constructor
A record is always given a constructor which matches its list of record components.
record Person(String name, int age) {}
void main() {
// This call to new Person(...) matches up with
// the record declaration.
var person = new Person("Ancient Dragon Man", 2000);
}
Similar to the "default constructor" given to regular classes, this is what you get for "free" with the declaration of a record.
Component Accessors
For each record component, an accessor method will be available that has the name of that component.
You use these to access the values of components.
record Dog(String name) {}
record Dog(String name) {}
class Main {
void main() {
var dog = new Dog("Hunter");
// .name() accessor is available
String name = dog.name();
System.out.println(name);
}
}
Component Accessor Visibility
The accessor methods of a record are always public, so all the packages that can see the class can access its components.
package dungeon;
public record Dragon(double wingspan) {}
import dungeon.Dragon;
void main() {
var dragon = new Dragon(224.5);
System.out.println(
// Method is visible.
dragon.wingspan()
);
}
Printing a Record
When printing out a record, the output will include each of the components of the record.
record Goblin(String name, int hp) {}
record Goblin(String name, int hp) {}
class Main {
void main() {
var goblin = new Goblin("Gobbo", 11);
System.out.println(goblin);
}
}
Goblin[name=Gobbo, hp=11]
This is more intelligable than what you would get by default from a regular class.1
Goblin@609db43b
It is possible to make a regular class print differently, but we'll get to that later.
Check for Equality
To check if the components of a record match with another, you can use the equals
method.
record Elf(boolean pretentious) {}
record Elf(boolean pretentious) {}
class Main {
void main() {
var elfOne = new Elf(true);
var elfTwo = new Elf(true);
System.out.println(elfOne.equals(elfTwo));
}
}
This is similar to how you check whether String
s are equal.
Return Multiple Values
One of the initial reasons I gave for wanting to use a class was returning multiple values from a method.
A record is likely better for that purpose than a regular class.
record Location(double latitude, double longitude) {}
Location findTreasureIsland() {
return new Location(40.2085, -3.713);
}
void main() {
Location treasureIsland = findTreasureIsland();
System.out.println(
"Treasure island is located at " +
location.latitude() +
" " +
location.longitude() +
"."
);
}
Regular classes are, of course, still useful. Its just for classes which only hold some data together (and nothing else interesting) getting a constructor and accessors automatically is very convenient.
Shorthand
One way to think about records is that they are "shorthand"1 for a regular class.
So the following record
public record Cat(boolean spayed, int weight) {}
is shorthand for a regular class that looks like this.
// There are a few parts that I left off here
// so this isn't 100% accurate.
public class Cat {
private final boolean spayed;
private final int weight;
public Cat(boolean spayed, int weight) {
this.spayed = spayed;
this.weight = weight;
}
public boolean spayed() {
return this.spayed;
}
public int weight() {
return this.weight;
}
// + the magic that makes it print nicer
// + the magic that lets you use .equals
// + a little more that will be relevant later
}
For you non-native English speakers, a shorthand is a shortened form of something. TTYL is "shorthand" for "Talk to you later."
Integers II
Integers never stop being relevant and, now that we have covered static methods, we are ready to cover some of the more useful ones that exist for working with integers.
Integer from a String
If you have a String
which contains text that can be interpreted
as an integer you can convert it to an int
using the parseInt
static method on Integer
.
void main() {
String text = "123";
int oneTwoThree = Integer.parseInt(text);
System.out.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
}
}
Integer to a String
If you have an integer you want to turn into a String
you have two options.
One is to "add it" to an empty string.
void main() {
int x = 4;
String xStr = "" + x;
System.out.println(xStr);
}
The other is to use the toString
static method on Integer
.
void main() {
int x = 4;
String xStr = Integer.toString(x);
System.out.println(xStr);
}
I personally find the second one more direct, but opinions can reasonably vary.
Base 16 Integer Literals
In polite society we use a base 10 number system1. This means
we start counting at 0
and go up by one until we reach 9
.
0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
Then when we go up one more we put a 1
at the front and "wrap around."
10
, 11
, 12
, etc.
In a base 16 number system, sometimes called "hexadecimal", you keep going a little bit more before wrapping around.
0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
, A
, B
, C
, D
, E
, F
, 10
, 11
, ..., A5
, ..., 5F
, ... etc.
To write an integer literal in Java which contains a hexadecimal number you write 0x
before the number.
void main() {
int sixteen = 0x10;
System.out.println(sixteen);
int twoHundredFiftyFive = 0xFF;
System.out.println(twoHundredFiftyFive);
}
Hexadecimal numbers like this are very common when you are writing code that
deals with colors in RGB - Red, Green, Blue - format. 0xFFFFFF
is white, 0xFF0000
is red, 0xCCCCFF
is periwinkle, etc.
Okay to be real with you, every number system is a base 10 system. Even if what you call "10" is what we would call "sixteen", you always wrap around your base when you write "10." If that doesn't make sense it doesn't matter, but its fascinating to me.
Integer from a Base 16 String
If you have a String
which contains text that can be interpreted as a base 16 integer, you can convert it into an int
by using parseInt
and giving the
number 16
as an extra argument.
void main() {
String text = "C";
int twelve = Integer.parseInt(text, 16);
System.out.println(twelve);
}
This will not work if the number is prefixed by 0x
like it would be in your code.
void main() {
Integer.parseInt("0xC", 16);
}
If you want to handle both hexadecimal numbers and regular base 10 numbers you should instead use Integer.decode
.
void main() {
System.out.println(Integer.decode("0xC"));
System.out.println(Integer.decode("0x19"));
System.out.println(Integer.decode("19"));
}
Integer to a Base 16 String
If you have an integer you want to turn into a String
in base 16
integer you can use the toHexString
method on Integer
.
void main() {
int x = 29411;
String xStr = Integer.toHexString(x);
// 72e3
System.out.println(xStr);
}
Underscores in Integer Literals
When you are writing large numbers 1000000000
isn't very
visually distinct from 10000000000
.
To help with the legibility you are allowed to insert underscores between digits in an integer literal.
void main() {
int x = 1_000_000_000;
int y = 10_000_000_000;
System.out.println(x);
System.out.println(y);
}
This works with hexadecimal integer literals as well.
void main() {
int white = 0xFF_FF_FF;
System.out.println(Integer.toHexString(white));
}
Files
Files are how you store information on a1 computer so that it can still be there when your program is done running.
As such, read files and writing files are tasks you will often want to do.
*normal
Paths
All files and folders on your computer can be located by a "path"
like "Downloads/movie.mp4
."
To store a path in a Java program we use the Path
class from the java.nio.file
1
package. You create these Path
s by giving a String
to the Path.of
static method.
import java.nio.file.Path;
class Main {
void main() {
Path pathToNotes = Path.of("notes.txt");
System.out.println(pathToNotes);
}
}
nio
stands for "new IO." So yes, there was an old IO. We will use that too. Its one of many artifacts from Java's interesting and sordid history.
IOException
If some code is "doing IO" - by which we mean while it is trying to read some Input or write some Output - you should expect it to throw an IOException
.
This class lives in the java.io
package so to use it by its simple name you need an import.
import java.io.IOException;
class Main {
void main() throws IOException {
throw new IOException("Something went wrong");
}
}
Since reading a file is reading some input and writing to a file is writing some output, this exception is relevant to reading and writing files.
UncheckedIOException
The unchecked version of an java.io.IOException
is UncheckedIOException
.
You can use this if you have a method which you don't want to propagate IOException
but
also want something more specific than RuntimeException
to re-throw.
And just like IOException
, if you don't want to write
out java.io.UncheckedIOException
more than once you need to add an import.
import java.io.IOException;
import java.io.UncheckedIOException;
class Main {
void main() {
try {
doStuff();
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Write to a File
To write a String
to a file you can use the Files
class from the java.nio.file
package. It has a static method named writeString
.
Path tasksPath = Path.of("tasks.txt");
String tasks = """
1. Do dishes
2. Do laundry
""";
Files.writeString(tasksPath, tasks);
This method can throw an IOException
, so to handle that you
can have a throws
declaration on the method calling it.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() throws IOException {
Path tasksPath = Path.of("tasks.txt");
String tasks = """
1. Do dishes
2. Do laundry
""";
Files.writeString(tasksPath, tasks);
}
}
The other option is to catch the IOException
and re-throw it as an unchecked exception.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() {
Path tasksPath = Path.of("tasks.txt");
String tasks = """
1. Do dishes
2. Do laundry
""";
try {
Files.writeString(tasksPath, tasks);
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Read from a File
To read a file's contents as a String
you can use the readString
static method
from java.nio.file.Files
.
Path tasksPath = Path.of("tasks.txt");
String tasks = Files.readString(tasksPath);
Similarly to Files.writeString
, this can throw an IOException
if
something goes wrong1.
This can be dealt with in the same way. Either declare the IOException
in a throws
clause or re-throw an unchecked exception.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() throws IOExeption {
Path tasksPath = Path.of("tasks.txt");
String tasks = Files.readString(tasksPath);
System.out.println(tasks);
}
}
If you choose to re-throw any IOException
as an unchecked exception then
it might be helpful to remember that delayed assignment is allowed in that context.2
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() {
Path tasksPath = Path.of("tasks.txt");
String tasks;
try {
tasks = Files.readString(tasksPath);
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
System.out.println(tasks);
}
}
Usually we can hand-wave this as "when something goes wrong it throws IOException
," but this is one of those cases where you might care about what exactly went wrong. Specifically, whether the reason you couldn't read a file because the file wasn't there. We'll use this method as an example when we go deeper into Exceptions
so there will be a chance to talk about that.
Mostly just because it can let you "shrink the scope" of the try-catch.
Creating a Folder
To create a folder, you can use Files.createDirectory
.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() throws IOExeption {
Path folderPath = Path.of("example1");
Files.createDirectory(folderPath);
}
}
This, like the other methods in Files
, might throw an IOException
.
Files.createDirectory
will fail if the folder already exists.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a file named hello.txt
and give it Hello, world
as contents.
Challenge 2.
Write a program that asks the user for a number and writes it into
a file named numbers.txt
.
Challenge 3.
Update the previous program so that the list of numbers entered by a user
is stored in the file. So if they give 1
, 2
, and 3
the file should contain
something like the following.
1
2
3
Hint: \n
is how you embed a newline character in String
. You might find it useful.
Challenge 4.
Update the previous program to also display the biggest number given so far
if the user types biggest
instead of a number.
Challenge 5.
Make the previous program behave sensibly if the file contains data that is not numbers.
Challenge 6.
Complete this program.
import java.nio.file.Path;
import java.io.IOException;
class Main {
record Person(String name, int age) {}
void main() throws IOException {
var people = new Person[] {
new Person("Steve Smith", 15),
new Person("Stan Smith", 42),
new Person("Rodger", 1601)
};
var path = Path.of("people.txt");
save(path, people);
people = load(path);
System.out.println(people[0]);
System.out.println(people[1]);
System.out.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
}
}
Object
You may have heard the phrase "everything is an object" parroted about.
It sounds very silly. Of course things are things, what else would they be?
In Java it has a very specific meaning. There is a class named
java.lang.Object
(Object
for short) which all other classes are
derived from.
Everything "is" an Object
.
Subtypes
We consider most everything to be a "subtype"
of Object
.
This means that if you have a variable or field that holds an Object
you can assign any data you want into it.
void main() {
String oak = "oak";
Object tree = oak;
System.out.println(tree);
}
If something is a subtype of Object
, we would call Object
its "supertype."1
Super meaning above and Sub meaning below. God how I feel for people who learn english as a second language.
instanceof
If you have an Object
you can recover the actual type
of the data stored in it using instanceof
.
void main() {
Object o = "123";
if (o instanceof String) {
System.out.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) {
System.out.println(
"Can call String methods after recovering the type: " + s.charAt(0)
);
}
}
toString
All Object
s have a toString
method.
This is intended to be a suitable representation for debugging, although one may choose to favor different concerns.
For built-in classes the result of their toString
method is likely
what you'd expect.
class Apple {}
void main() {
Object o = "123";
// If its already a String, toString() doesn't
// have to do much work
System.out.println(o.toString());
o = 123;
// Integers, Longs, etc. all have a representation
// which looks the same as they do in literal form.
System.out.println(o.toString());
o = new Apple();
// And custom classes will, by default, just have the
// class name followed by gibberish
System.out.println(o.toString());
}
Override toString
To customize the behavior of toString
in your own classes you need to "override"
the toString
method from Object
.
What this means is that you need to define a method
which has the same name, arguments, return type, and visibility
as the one defined in Object
.
That is, a public
method named toString
which takes no
arguments and returns a String
.
class Window {
public String toString() {
return "Window!";
}
}
void main() {
Object o = new Window();
System.out.println(o);
}
This is how you can customize the output of System.out.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);
System.out.println(o);
}
@Override
If you intend to override a method you should put
@Override
above that method.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Position[x=" + x + ", y=" + y + "]";
}
}
void main() {
Object o = new Position(9, 8);
System.out.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);
System.out.println(o);
}
equals and hashCode
In addition to toString
, two methods that all Object
s have defined
are equals
and hashCode
.
equals
is a method that takes another object and returns a boolean
based
on whether you would consider those objects to be equivalent.
By default, equals
will behave identically to ==
.
class Thing {}
class Main {
void main() {
var t1 = new Thing();
var t2 = new Thing();
System.out.println(t1 == t1);
System.out.println(t1.equals(t1));
System.out.println(t2 == t2);
System.out.println(t2.equals(t2));
System.out.println(t1 == t2);
System.out.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;
System.out.println(a.equals(b));
System.out.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";
System.out.println(a.hashCode());
// a.equals(b) will return true, so they will have the same hash code
System.out.println(b.hashCode());
// a.equals(c) will return false, so they may or may not have the same hash code
System.out.println(c.hashCode());
Thing t1 = new Thing();
Thing t2 = new Thing();
// The default .equals() is the same as ==
System.out.println(t1.hashCode());
System.out.println(t2.hashCode());
}
}
Override equals and hashCode
If you want to customize the equals
method of an object the
general pattern for doing so is the following.
First, declare an equals method that matches the one that comes from Object
.
Don't forget @Override
.
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
}
}
This definition of equals accepts any Object
as an argument. Therefore we first
need to make sure that the class of that Object
is the same as the class you are
comparing it to.
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
if (o instanceof Position otherPosition) {
}
else {
return false;
}
}
}
Then you compare the all the fields to make sure they are equal to each other as well.
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
if (o instanceof Position otherPosition) {
return this.x == otherPosition.x && this.y == otherPosition.y;
}
else {
return false;
}
}
}
How you do those comparisons depends on what is in the fields. For int
and such you can use ==
. For fields
like String
you need to use .equals
.
class Tree {
String barkDescription;
@Override
public boolean equals(Object o) {
if (o instanceof Tree otherTree) {
//
return this.barkDescription.equals(otherTree.barkDescription);
}
else {
return false;
}
}
}
If you anticipate, or don't take actions to prevent, a field potentially being null
then instead of a.equals(b)
use java.util.Objects.equals
. All it does special is handle
null
without crashing.
import java.util.Objects;
class Tree {
String barkDescription;
@Override
public boolean equals(Object o) {
if (o instanceof Tree otherTree) {
return Objects.equals(otherTree.barkDescription);
}
else {
return false;
}
}
}
Whenever you override equals
you are supposed to override hashCode
as well.
This is because - by default - every Object
is going to give a hashCode
consistent
with the default equals
method. If you change how equals
works, then you could
violate the contract of "if hashCode gives a different value, they definitely aren't equal."
Some parts of Java will rely on that, so its worth addressing.
If you define your equals
method as above - essentially "they are equal if all their fields are equal" - then you can use java.util.Objects.hash
to define your hashCode
.1
import java.util.Objects;
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
if (o instanceof Position otherPosition) {
return this.x == otherPosition.x && this.y == otherPosition.y;
}
else {
return false;
}
}
@Override
public int hashCode() {
// Just give it all the fields you have.
return Objects.hash(this.x, this.y);
}
}
If you defined a more exotic form of equals
then how to properly make a hashCode
is an exercise for you, the reader. If you give up then return 1;
will always be "correct," if not ideal for the code
that uses hashCode
as a quick "might be equal" check.
Generics
Certain types of classes, like growable arrays, are simply holders of data.
That is, almost none of how they work has to change to store different kinds of data.
Generics help us make these generically useful containers.
class Box<T> {
T value;
}
Type Variables
After the name of a class in the class definition you can put one or more "type variables."
These look like <
followed by a comma separated list of "type names" and ended by a >
.
class Box<T> {
}
Inside a class definition you can use these type variables on fields, method arguments, and method return types.
class Box<T> {
T data;
void setData(T newData) {
this.data = newData;
}
}
This indicates that you would be okay with any type being used in place of the type variable (in the above code T
).
Naming
Type variables don't have to be only a single letter, though that is common. If you pick a longer name you are generally expected to name them as if they were classes.
class Box<Data> {
Data data;
}
Instantiation
When you make an instance of a class that takes generic parameters you are expected to provide the actual concrete types you want to be used in place of any type variables.
You do this by writing the name of the types inside a <>
alongside the call to new
.
class Box<T> {
T data;
}
void main() {
var boxOfString = new Box<String>();
boxOfString.data = "abc";
String s = boxOfString.data;
System.out.println(s);
}
Inference
Sometimes Java has enough information to know what the right values for type variables would be without you specifying them.
In these cases you can rely on the compiler to infer them by writing <>
with no type variables
in between.
class Box<T> {
T data;
}
void main() {
// It is being assigned to a Box<String> on the left
// so Java can figure out what should be on the right.
Box<String> boxOfString = new Box<>();
boxOfString.data = "abc";
String s = boxOfString.data;
System.out.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;
System.out.println(s);
}
Soundness
Even though every String
is assignable to Object
,
a Box<String>
is not assignable to Box<Object>
.
If that was allowed then you could do something like the following.
Box<String> stringBox = new Box<>();
// This might feel innocent
Box<Object> objectBox = stringBox;
// But now we can put anything,
// like an Integer, in the Box
objectBox.data = 123;
// Which is a problem since that affects
// stringBox as well
String s = stringBox.data; // But its actually an Integer! Crash!
We call this property - that the types don't let you accidentally make silly situations - soundness. Java isn't fully sound, but its sound enough for most people.
Raw Types
If generics are cramping your style, you are technically allowed to turn them off.
If you make an instance of a generic class without
any <>
we call that a "raw type."
class Box<T> {
T data;
}
void main() {
Box b = new Box();
}
When you have a raw type you will see Object
in any place
you put1 a type variable.
This lets you do whatever you want without the burden of having to make sense to Java.
class Box<T> {
T data;
}
void main() {
Box b = new Box();
b.data = 123;
b.data = "abc";
if (b.data instanceof String s) {
System.out.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 I haven't shown you any yet. Avoid doing so yourself - at least for awhile.
- Generics weren't always in Java! Classes that later were made generic had to stay compatible with old "raw" usages somehow.
All that is to say: Be aware raw types exist. Make sure you are always putting <>
otherwise you are falling
into that mode. Avoid that mode.
An "unbounded" type variable to be exact. We'll visit generic bounds later.
Interfaces
Generics let you write code that doesn't care
what is different between different things - you would accept a String
or an Integer
, doesn't matter what is diferent between them.
Interfaces do a related thing. They let you write code that takes advantage of commonalities.
inteface Dog {
void bark();
}
Interface Declaration
To declare an interface you write the word interface
followed by the name of the interface
and {}
.
interface Dog {}
Inside of the {}
you can write the signatures for methods followed by a ;
. That means
no method body and no public
or private
modifiers.
interface Dog {
void bark();
String fetch(String ball);
}
Implementation
All interfaces do1 is hold method declarations.
If you want to have a class which "implements"
the interface you can do so by writing implements
followed by the interface
name.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
}
Then you 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() {
System.out.println("Bark");
}
public String fetch(String ball) {
return ball + " (with drool)";
}
}
For now*
All methods that come from an interface must be public
.
@Override
Just like when defining your own equals
, hashCode
, and toString
you
can use @Override
when implementing methods that come from an interface.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
System.out.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
Right now there isn't a mechanical use for this since Java will yell at you if you defined the interface method wrong anyways, but there will be later. A small benefit is that it makes it easier to tell at a glance which methods come from an interface and which do not.
Naming
Interfaces are named in the same way as classes - LikeThis
.
In the wild west of the real world you might see people prefix any interface
name with I
, for interface.
In these cases instead of Dog
you would see IDog
. Instead of PartyAnimal
you would
see IPartyAnimal
and so on.
The reason someone might do this is if they think it is worthwhile to have a visual indicator of whether a type represents an interface or an actual class. Personally, I don't think that is too useful, but there is nothing horrible about it.
Just judge the social context you are in and don't hold it against people too hard if they do it a way you don't like.
Subtypes
Just as everything is a subtype of Object
, anything which
implements an interface is a subtype of that interface.
For the following code this means that any field or variable
which holds a Dog
can be assigned to an instance of Mutt
.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
System.out.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() {
System.out.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
void main() {
Dog dog = new Mutt();
dog.bark();
System.out.println(dog.fetch("Ball"));
}
Multiple Implementations
Interfaces can be implemented any number of times. This means that code which accepts an interface can't truly know the specifics of how methods will work.
This is a problem if you want maximum predictability but its also the whole point of using an interface over a regular class. You can write code that depends on a few key methods being defined and be flexible to different ways of defining those methods.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
System.out.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
class Cat implements Dog {
@Override
public void bark() {
System.out.println("Meow");
}
@Override
public String fetch(String ball) {
return "no.";
}
}
void barkAndFetch(Dog dog) {
dog.bark();
System.out.println(dog.fetch("Ball"));
}
void main() {
barkAndFetch(new Mutt());
barkAndFetch(new Cat());
}
Time
Wikipedia defines "time" as "The continued sequence of existence and events that occurs in an apparently irreversible succession from the past, through the present, and into the future."1
Most everything that interacts with the real world needs to understand information about time. As such, Java has various ways to work with data that holds information about time.
https://en.wikipedia.org/wiki/Time
Instant
A java.time.Instant
stores information on a particular moment in time.
You can get the current "instant" with Instant.now
.
import java.time.Instant;
void main() {
var now = Instant.now();
System.out.println(now);
}
But if you happen to know a number milliseconds after January 1, 1970 UTC1 you
can get an Instant
which represents that point in time with Instant.ofEpochMilli
.
import java.time.Instant;
void main() {
var january2nd = Instant.ofEpochMilli(86400000);
System.out.println(january2nd);
}
https://en.wikipedia.org/wiki/Unix_time
Duration
A Duration
stores a duration of time.
You can make these with Duration.ofMinutes
, Duration.ofMillis
and other similarly named methods.
import java.time.Duration;
void main() {
var fiveMinutes = Duration.ofMinutes(5);
System.out.println(fiveMinutes);
var twelveMilliSeconds = Duration.ofMillis(12);
System.out.println(twelveMilliSeconds);
}
You can use these get the duration between two Instant
s with
Duration.between
.
import java.time.Instant;
import java.time.Duration;
void main() {
var january2nd = Instant.ofEpochMilli(86400000);
System.out.println(january2nd);
var january3rd = Instant.ofEpochMilli(86400000 * 2);
System.out.println(january3rd);
Duration twentyFourHours = Duration.between(january2nd, january3rd);
System.out.println(twentyFourHours);
}
And you can move Instant
s by a given Duration
of time using its .plus
and .minus
methods.
import java.time.Instant;
import java.time.Duration;
void main() {
var january1st = Instant.ofEpochMilli(0);
System.out.println(january1st);
System.out.println(
january1st.plus(Duration.ofHours(45))
);
System.out.println(
january1st.minus(Duration.ofHours(1))
);
}
LocalDate
A local date is something like "January 10th, 2024."
This just records a day, month, and year. It doesn't know in what part of the world it was January 10th 2024, just that somewhere the "local" date was that.
You can make a LocalDate
with LocalDate.of
.
import java.time.LocalDate;
void main() {
var jan10 = LocalDate.of(2024, 10, 1);
System.out.println(jan10);
}
And you can get the current LocalDate
in whatever timezone
your computer is programmed to be in with LocalDate.now();
import java.time.LocalDate;
void main() {
var now = LocalDate.now();
System.out.println(now);
}
LocalTime
In the same way a local date leaves off context about where it was and what time it was, a local time just stores the time.
This means it will hold the exact time during the day, down to the nanoseconds.
You can make a LocalTime
with LocalTime.of
giving it the hours, minutes, and
seconds into the day.
import java.time.LocalTime;
void main() {
var tenTwentyFour = LocalTime.of(10, 24, 0);
System.out.println(tenTwentyFour);
}
And similarly you can get the current time your computer thinks it is with LocalName.now()
import java.time.LocalTime;
void main() {
var now = LocalTime.now();
System.out.println(now);
}
LocalDateTime
What do you get if you combine a LocalDate
and a LocalTime
? A LocalDateTime
!
A LocalDateTime
stores dates and times.
If you have both a LocalDate
and a LocalTime
you can combine them
with LocalDateTime.of
.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
class Main {
void main() {
var jan10 = LocalDate.of(2024, 10, 1);
var tenTwentyFour = LocalTime.of(10, 24, 0);
System.out.println(LocalDateTime.of(jan10, tenTwentyFour));
}
}
Time Zones
Partially because of how day night cycles work and sometimes because of arcane rules made up to help out farmers, we have time zones.
This means that while it might be 9am in Boston it would simultaneously be 10pm in Tokyo. Boston and Tokyo are in different time zones.
You can get access to the time zone your computer
is using with ZoneId.systemDefault()
. This gives you
a ZoneId
which identifies a time zone.
import java.time.ZoneId;
class Main {
void main() {
ZoneId tz = ZoneId.systemDefault();
System.out.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");
System.out.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");
System.out.println(tz.getRules());
}
}
And it is this machinery used by the parts of the time API which determine the exact time it would be in a given time zone.
ZonedDateTime
A ZonedDateTime
has all the information of
a LocalDateTime
, but with the addition of a time zone.
These are useful for recording the time that events took place in a way that can be communicated.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
class Main {
void main() {
var jan10 = LocalDate.of(2024, 10, 1);
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);
System.out.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();
System.out.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"));
System.out.println(now);
}
}
OffsetDateTime
ArrayList
Java comes with a generic growable array. It is called
an ArrayList
.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("John Wick");
System.out.println(names);
}
Ubiquity
Java comes with many classes. Some of these
you might use a few times (like LocalTime
), some you
will see every day of your coding life (like String
).
ArrayList
is in the second category. It turns out
that a growable list of things is needed for a lot
of different tasks. It is ubiquitous in code that
you will see in the real world.
I mention this mostly to make sure you are paying attention. Its not particuarly hard, but try not to zone out on these sorts of chapters.
Add an item
To add an item to an ArrayList
you use the .add
method.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("The Bowery King");
names.add("Caine");
System.out.println(names);
}
The way this works is conceptually the same as the growable array that we went over earlier.
All you need to know though is that you can add as many items as you will and the container will grow dynamically.
Size
The 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<>();
System.out.println(names.size());
names.add("Vincent Bisset de Gramont");
System.out.println(names.size());
names.add("Mr. Nobody");
System.out.println(names.size());
}
This will tell you the number of elements in the ArrayList
but
not the amount of space allocated by the underlying array.
This is fine because, if you aren't the one making a growable array, it doesn't matter. All you need to concern yourself with is what is actually there, not any book keeping.
Get an item
To get an item at a given index in an ArrayList
you should use .get
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("Winston Scott");
String name = names.get(0);
System.out.println(name);
}
If the index you picked is greater than the number of elements in the ArrayList
it will throw an IndexOutOfBoundsException
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("Killa Harkan");
String name = names.get(10);
System.out.println(name);
}
Loop over Contents
Just like the .length
and []
operations on arrays,
you can loop over all the elements in an ArrayList
with the combination of .size()
and .get(idx)
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("Sofia Al-Azwar");
names.add("Viggo Tarasov");
names.add("Iosef Tarasov");
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
System.out.println("NAME: " + name);
}
}
Its not a rule, but this is also a convenient use of for loops. You can use all the same tricks you learned when looping over arrays here.
Set an item
You can set an item at an index with .set
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("John Wick");
names.set(0, "Baba Yaga");
System.out.println(names);
System.out.println(names.get(0));
}
If the index you provide is out of bounds, you will get an IndexOutOfBoundsException
.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("John Wick");
names.set(15, "Baba Yaga");
}
Remove an item
You can use .remove
to remove an item
from an ArrayList
.
To do this you need to provide it the value you want to remove. It will do all the array resizing required internally, even if the item is in the middle of a list.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("The Bowry King");
names.add("The Elder");
names.add("The Harbinger");
System.out.println(names);
names.remove("The Elder");
System.out.println(names);
}
Alternatively you can remove an item by its index.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("The Bowry King");
names.add("The Elder");
names.add("The Harbinger");
System.out.println(names);
names.remove(2);
names.remove(0);
System.out.println(names);
}
You need to be careful about that though. If your ArrayList
holds Integer
s then
Java can get confused between the implementation of remove that removes using the item itself
and the one that removes by index.
import java.util.ArrayList;
void main() {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
System.out.println(numbers);
// Notice that this removes "2" which is at index 1!
numbers.remove(1);
System.out.println(numbers);
}
This comes down to int
and Integer
being slightly different. When Java
sees an integer literal it assumes it is an int
. It is only when circumstances
are such that it cannot possibly be an int
that it automatically boxes it
into an Integer
.
It is rare, but if you encounter this issue you can use Integer.valueOf
to deal with it.
import java.util.ArrayList;
void main() {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
System.out.println(numbers);
numbers.remove(Integer.valueOf(1));
System.out.println(numbers);
}