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

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.

Picture of the sign up button on replit's website

Click it and sign up for an account.

Picture of the sign up form on replit's website

Step 2. Create a Java REPL

Find the "Create REPL" button and click it.

Picture of the create repl button on replit's website

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".

Unfilled in create from template menu on replit

Filled in create from template menu on replit

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".

Picture of an unran hello world program

Click that run button, and you should see the text Hello, world! appear under the console window.

Picture of a hello world program after running

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);
}
1

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";
}
1

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.

haveOreosInHousestuckToCalorieLimit
falsetrue
truefalse

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.

funToBeAroundbelievesInFundamentalHumanRightswillAskOnDate
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

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.

dogLooksNicepersonLooksNicewillAskToPetDog
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

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.

hasIceCreamhasCookievalidChoice
truetruefalse
truefalsetrue
falsetruetrue
falsefalsefalse

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 ints 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 ints 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 ints using the * operator.

void main() {
// x will be 15
int x = 3 * 5;
// y will be 75
int y = x * 5;
// z will be 1125
int z = x * y;

System.out.println(x);
System.out.println(y);
System.out.println(z);
}

Division

You can divide any two ints 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 ints can be inspected to see if their value is equal by using the == operator.

Unlike the previous operators, which all take ints and produce ints as their result, == takes two ints and produces a boolean as its result.

void main() {
// 1 is never equal to 2
// this will be false
boolean universeBroken = 1 == 2;
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 !=, ints 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;
1

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);
}
1

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.

  1. 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.
  2. 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 doubles 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 doubles 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 doubles using the * operator.

void main() {
double x = 3;
// y will be 27
double y = x * 9;
// z will be 13.5
double z = y * 0.5;

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 doubles using the / operator.

void main() {
double x = 8;
// y will be 4.0
double y = x / 2;
// z will be 1.3333333333333333
double z = y / 3;

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

1

With the caveat that the result is now potentially inaccurate.

Equality

Just like ints, doubles 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 !=, doubless can be compared to see if one is bigger than another using >, <, >=, and <=.

This works the same as it does with ints.

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 doubles the same as they do with ints.

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.

1

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 ints are representable as doubles so it is a "widening conversion" and will be handled automatically by Java when performing an assignment.

void main() {
int x = 5;
double y = x;

System.out.println(x);
System.out.println(y);
}

This is not true in an expression. Even if the result of a computation between ints is being assigned to a double, the computation will still be performed using the same rules ints usually follow.

void main() {
int x = 7;
int y = 2;
// integer division of 7 and 2 gives 3.
double z = x / y;

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 chars 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);
}

chars will be automatically converted to ints when used with mathmatical operators like +, -, >, <, etc.

void main() {
char gee = 'g';

// all the letters from a to z have consecutive numeric values.
boolean isLetter = gee >= 'a' && gee <= 'z';

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.

1

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 = '👨‍🍳';

chars are actually "utf-16 code units". Many symbols require multiple "code units" to represent.

For a full explanation, refer to this old Computerphile video.

It describes "utf-8", which is 8 bits per "code unit." Java's char uses 16 bits, but that is the only difference.

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 Strings.
  • 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 Strings 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 chars 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"
);
}
1

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);
}
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 printlns, change one line in this program so that it outputs racecar.

Try to find two ways to do that.

void main() {
    String racecar = "racecar";

    int diff = 1;

    int index = 6;

    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.

1

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 ifs 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 ifs as you need. Each one will only run if all the previous conditions evaluate to false.

void main() {
boolean cool = true; // 🕶️
int age = 100; // 👴

if (age < 25) {
    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
1

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

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

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"
);
}
1

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";
}
1

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++;
}
}
1

What [I@5a07e868 and co. mean isn't really important. Try not to get too distracted by it.

2

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.

  1. An initializer.

  2. An expression which evaluates to a boolean.

  3. 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

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 Strings 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.

1

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 ints and Strings, but if you are feeling clever you can use this ability along with arrays or other things you can change without reassigning a variable.

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

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

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();
}
1

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

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

1

"Overloading" in this context means when one word has more than one possible meaning depending on how it is used. Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.

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 doubles to ints, you must do it yourself.

double returnNine() {
    double nine = 9.0;
    // The (int) explicitly converts the double to an int
    return (int) nine;
}

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();
}
1

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;
}
1

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

1

We call them "primitive" types because there isn't a way for you to implement them yourself in Java. They have to be given to you as a fundamental and magic sort of thing.

2

Don't worry too much about the word box in this context. This will make more sense once you learn how to define your own types. I just wanted to at least try to gesture at why it has the silly name that it does.

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 Integers directly in math expressions.

void main() {
Integer x = 5;
int y = 3;
int z = x * y;

System.out.println(z);
}

As well as Booleans 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);
}
1

This might feel obvious, but this is one of the few places in Java where the type of something "magically" changes. int and Integer, char and Character, etc. are different types.

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];
}
}
1

The reasons for this are deeply interesting and have to do with the nitty gritty of how Java is actually implemented. It might also change in the future.

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 ].

1

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]);
}
1

Fun fact. The GameBoy and GameBoy Advance Pokemon games tracked pokedex completion in a big boolean array. If you saw a Pokemon it would flip that Pokemon's index in the "seen" array to true. If you caught it, it would do the same in a different array. Those games weren't written in Java, but the concept is the same.

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

1

What an idiot

2

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 {

}
1

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.

1

I haven't used it in many code samples thus far, but if you remember var this is one of the times where it can be aesthetically convenient. var kermit = new Muppet();

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

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 +
            "."
    );
}
1

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.

1

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();
}
1

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

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();
}
1

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
}
1

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 booleans.

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 Strings 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.

1

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 Strings contain the same contents but you do not care if those contents differ in the casing of letters, you can use the .equalsIgnoreCase method.

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 Strings.

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.

1

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");
    }
}
1

Generally you will just use e for this. Even if you don't call any instance methods on the exception, you still need to give a name for it.

2

Technically you can have a try without a catch, but only when using another feature of Java you haven't been shown yet. It will make sense when the time comes.

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 ints 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 ifs and else ifs.

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();
}
1

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);
}
1

This is primarily useful for "constant" values. You will need these, but having constants attached to instances is a bit unique and won't happen that often.

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.

1

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);
}
1

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);
}
1

This explanation is I think a correct description of what it means to be global, but what I am showing won't really hold up as a global thing the more you learn.

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();
}
1

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.

1

Software Development is still a relatively new field. Some things we take for granted may end up not being true after all. It doesn't help that its also pretty poorly researched. Keep an eye out for that.

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

1

There is a whole sect of folks who write their programs as actual books. Look up "Literate Programming" to dive deeper in to that.

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

1

https://arxiv.org/abs/2302.00009

2

No offense intended if you are still having trouble with loops. Its common, you will eventually get past it.

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;
    }
}
1

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 + ".");
}
1

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.

1

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.

1

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.

1

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

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.

1

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.

1

https://en.wikipedia.org/wiki/United_States_v._Microsoft_Corp.

2

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

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

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

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 pwd1 command.

$ pwd
/Users/emccue
1

"Print Working Directory"

Listing Files

You can list the files in your current working directory with the ls1 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.

1

Short for "List"

Creating Directories

In order to create a directory you can use the mkdir1 command.

$ mkdir project
$ ls
project
1

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
1

Short for "change directory"

Creating Files

To create a blank file you can use the touch1 command.

$ touch hello.txt

This is situationally useful, but more often than not you will create files using a program called a "text editor."

There are some text editors, like vim, which run entirely inside the terminal.2

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.

1

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
1

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.

1

This rule is merely a suggestion and people's definitions of "something intelligent" and "recover" vary wildly. Expect some things to throw checked exceptions and others to not and just know that you need to check for the checked ones.

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() {
    
}
1

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.

1

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!");
}
1

people have the audacity to equate vanilla with “plain”. the fruit of a delicate orchid pollinated by hand. worth its weight in solid gold and beyond. the fussy black-and-cream jewel of the american continent. you sick son of a bitch. imagine a world without vanilla. no blondies. no pound cakes. no crème brûlée, no coke floats. no cream soda. no satiny new york-style cheesecakes. no warm apple pie à la mode. no velvety complexity to bring out complex notes in chocolate desserts. no depth of flavour in your cakes and cookies and milkshakes. all in just a few precious seeds or grams of paste or perfumed teaspoons of liquid black platinum. what you don’t understand could fill the library of alexandria seven times over and then some. you ungrateful bastard i’m going to kill you - Tumblr user "kirbyofthestars"

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.

1

Technically we are talking about an "expression context." Meaning a place where you are allowed to put an expression. The right hand side of an equals sign is one, but there are many others.

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 yields 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
1

There are different ways to layout a project. All are valid. You can call the src folder STUFF - it ultimately doesn't matter. This is just another one of those social conventions.

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

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 Strings 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()
    );
}
1

Not that the categorization matters much, but socially we expect "accessor"-looking methods to only give you a value and not "do stuff" like increment a number or mess with a file.

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
    }
}
1

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;
}
1

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

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);
    }
}
1

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.

1

This data structure is crazy important. Maybe the most common one used in the world. Java has it built-in and we'll get to that later.

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++;
    }
}
1

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
1

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.

1

A theme that will start to emerge is that the "best" code sometimes has a few extra modifiers on it and the "default" behavior isn't what you want. Static inner classes are way less magic.

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.

1

People also often call this the "FQCN". Its a fun acronym to write, but I have no clue how to say it out loud. "Faquacün?"

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.

1

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);
    }
}
1

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;
    }
}
1

Your spider-sense might be tingling wondering if private constructors are a thing. They are! I'll talk about them more in-depth later, but they can be surprisingly useful.

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.

1

That might be mixed with other code written by different companies or by different people.

2

This is why many tools will have your starter project have classes in the com.example package

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() {}
1

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
1

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 Strings 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
}
1

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.

1

Okay to be real with you, every number system is a base 10 system. Even if what you call "10" is what we would call "sixteen", you always wrap around your base when you write "10." If that doesn't make sense it doesn't matter, but 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.

1

*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.file1 package. You create these Paths by giving a String to the Path.of static method.

import java.nio.file.Path;

class Main {
    void main() {
        Path pathToNotes = Path.of("notes.txt");

        System.out.println(pathToNotes);
    }
}
1

nio stands for "new IO." So yes, there was an old IO. We will use that too. Its one of many artifacts from Java's interesting and sordid history.

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);
    }
}
1

Usually we can hand-wave this as "when something goes wrong it throws IOException," but this is one of those cases where you might care about what exactly went wrong. Specifically, whether the reason you couldn't read a file because the file wasn't there. We'll use this method as an example when we go deeper into Exceptions so there will be a chance to talk about that.

2

Mostly just because it can let you "shrink the scope" of the try-catch.

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

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 Objects 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 Objects have defined are equals and hashCode.

equals is a method that takes another object and returns a boolean based on whether you would consider those objects to be equivalent.

By default, equals will behave identically to ==.

class Thing {}

class Main {
    void main() {
        var t1 = new Thing();
        var t2 = new Thing();

        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);
    }
}
1

If you defined a more exotic form of equals then how to properly make a hashCode is an exercise for you, the reader. If you give up then return 1; will always be "correct," if not ideal for the code that uses hashCode as a quick "might be equal" check.

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

  1. 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.
  2. Generics weren't always in Java! Classes that later were made generic had to stay compatible with old "raw" usages somehow.

All that is to say: Be aware raw types exist. Make sure you are always putting <> otherwise you are falling into that mode. Avoid that mode.

1

An "unbounded" type variable to be exact. We'll visit generic bounds later.

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)";
    }
}
1

For now*

2

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.

1

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);
}
1

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 Instants 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 Instants 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 Integers then Java can get confused between the implementation of remove that removes using the item itself and the one that removes by index.

import java.util.ArrayList;

void main() {
    ArrayList<Integer> numbers = new ArrayList<>();
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);

    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);
}