Java Basics

Java is a general-purpose text-based programming language popular in such diverse application areas as banking and Android applications. Let’s take a moderately technical tour of the simpler parts of the language.

Getting Started

This page is part tutorial, part reference. It focuses on the technical side of things, but is meant to be interactive! There are two prerequisites:

All set? Fire up JShell and warm up with a few simple expressions:

jshell> 2 + 2    ARITHMETIC
4

jshell> 2 > 2    BOOLEANS
false

jshell> 3 + 100 * Math.sqrt(25)    OPERATOR PRECEDENCE
503.0

jshell> "Hello".replace('e', 'u')    STRINGS AND CHARACTERS
"Hullo"

jshell> List.of(3, 5, 3, 2, 2, 1, 8, 1, 1)    LISTS
[3, 5, 3, 2, 2, 1, 8, 1, 1]

jshell> Map.of("dog", 3, "rat", 5, "pig", 99)    DICTIONARIES
{rat=5, dog=3, pig=99}

jshell> var greeting = "Good morning"    CREATING VARIABLES
"Good morning"

jshell> greeting.substring(5)    USING VARIABLES
"morning"

Type along as you read through these notes. If you feel inquisitive and want to explore a bit more, type in whatever you feel inspired to try out.

Types

Programming is all about manipulating units of information called values (e.g., 233, false, "Invalid credentials"). To make these manipulations comprehensible, values are grouped into types (e.g., int, boolean, String), which define what we can and cannot do with the values.

Java gives us eight primitive types and five mechanisms for creating new types. The primitive types and the type formers are listed here, for the sake of completeness only, as they will all be described in more detail later (with examples in context). For now, we are just trying to get the big picture up front:

Primitive TypeDescriptionExample Values of the Type
booleanThe two values true and falsefalse
charCode units in the range 0..65535 that are used to encode text'π'
byteIntegers in the range -128 ... 127(byte)89
shortIntegers in the range -32768 ... 32767(short)89
intIntegers in the range -2147483648 ... 214748364789
longIntegers in the range -9223372036854775808 ... 922337203685477580789L
floatLower precision floating point numbers3.95f
doubleHigher precision floating point numbers3.95
Type FormerDescriptionExample Type
classClasses
class Task {
    int id;
    Person assignee;
    String description;
    DateTime due;
}
[]Arrays (a special kind of class)boolean[]
enumEnums (a special kind of class)
enum Confidence {
    LOW,
    MEDIUM,
    HIGH
}
recordRecord (just a shorthand way for making certain classes)
record Badge(int level, String name) {}
interfaceInterfaces
interface Savable {
    void save();
}
See anything missing?

If you come from a Python or JavaScript background, you might be asking “What, no string type?” or “What, no function type?” or “What, no unbounded integer type?”

Relax, Java has them: they’re just created with classes; we’ll see how soon.

Variables

If you initialize a variable at the point of its declaration, just use var:

jshell> var numberOfColors = 256 * 256 * 256
16777216

jshell> var moreThanAMillionColors = numberOfColors > 1000000
true

jshell> var rubyAuthor = "まつもと ゆきひろ"
"まつもと ゆきひろ"

In Java, type constraints are applied to variables when they are declared:

jshell> var friends = 39
0

jshell> friends = 40
12

jshell> friends = true
|  Error: incompatible types: boolean cannot be converted to int

Type constraints must be set, so you can’t use var unless you have an initializer:

jshell> var payment
|  Error: cannot use 'var' on variable without initializer

You can, however, declare a variable without an initializer provided you specify the type constraint:

jshell> int books
0

jshell> double price
0.0

jshell> boolean found
false

jshell> String message
null

WAIT WHAT EVEN IS THAT null THING? The strange value null is really weird: it is a member of all non-primitive types! So a string variable can have the value null, as can variable of lists, arrays, and so on. This is really unfortunate. It also means that null by itself isn’t enough to determine a type:

jshell> var supervisor = null
|  Error: cannot infer type (variable initializer is null)

We’ve seen that Java will generally infer the type constraints for variables when it can, but will require you to manifest constraints when it cannot. You are also required to manifest constraints when inference is just really hard, too! Consider parameters and return values of methods (what Java calls functions). Inferring type constraints on parameters is hard because you have to look for all the calls to see what values may be passed in. Inferring constraints on return values is hard because a method may be very long and have multiple return statements. To keep things simple, Java simply forces you have to manifest those constraints:

jshell> boolean isEven(int n) { return n % 2 == 0; }
|  created method isEven(int)

jshell> isEven(90521188)
true

Printing

When you evaluate an expression in JShell, JShell will display the value for you. Sometimes you may prefer to print the value yourself, with System.out.println. These outputs will normally be the same (though they differ a bit for strings!). Printing is very useful when you have more than one value to print:

jshell> "hello"
"hello"

jshell> System.out.println("hello")
hello

jshell> var dogs = List.of("Luna", "Gypsy", "Lisichka")
[Luna, Gypsy, Lisichka]

jshell> for (var d: dogs) { System.out.println("I like " + d); }
I like Luna
I like Gypsy
I like Lisichka

System.out.println is going to be your friend when you write Java applications. You should also learn about System.out.printf. This takes a format string as its first argument and interpolates the rest of the arguments into the format specifiers. There are zillions of formatting options, but here’s one quick example just to give you an idea:

jshell> System.out.printf("Hello %d: %g ^^^ %10.3f|%15s\n", 10, 95.0, -1.1,  "abc")
Hello 10: 95.0000 ^^^     -1.100|            abc

Numbers

Java numbers are written just like numbers in JavaScript and Python: they have digits, use 0b for binary, 0x for hex, and E for “times ten to the”:

jshell> 55
55

jshell> 0b1000101
69

jshell> 0x1a4
420

jshell> 9.51E4
95100.0

jshell> 23.5E-3
0.0235

The addition (+), subtraction (-), multiplication (*), division (/), and remainder (%) operations follow the usual order of operations:

jshell> 7 + (10 / 5) * 3 - 1 * 8 % 3
11

So many numeric types

Six of the primitive types are numeric (int, double, byte, short, long, float), and two non-primitive types (BigInteger, BigDecimal) exist in the standard package java.math.

Java numbers are messier and more complicated than those of JavaScript or Python. But DON’T PANIC! Take it slow. Start with these two easy-to-remember facts:

  1. If you write a simple number with just digits, it has type int; if you add just a decimal point or an E (or both) it has type double. Examples:
    • 89 is an int
    • 89.0, 89.9, 1E6, -35.2E11 are doubles
  2. Almost all the time, you will just use int and double. The other types are only used in pretty specialized application areas (like systems programming, multimedia, and graphics libraries).

If you know some other programming languages, here are some equivalences:

JavaPythonJavaScriptSwift
intInt32
BigIntegerintBigInt
doubleFloatNumberDouble
BigInteger and BigDecimal are different

The types BigInteger and BigDecimal do not use +, -, * and friends. Details on these types are outside the scope of these notes. We’re only doing int and double today!

Java numbers can overflow—be careful!

Java numbers are not like numbers in math: there are only a finite number of them. Integers, for example, are bounded in the range -2147483648...2147483647; if an operation overflows, things wrap around (meaning after 2147483647 they start over again at -2147483648):

jshell> 2147483642 + 8
-2147483646

jshell> 2000000000 + 2000000000
-294967296

If that range is too small, you can use longs; they go from -9223372036854775808 to 9223372036854775807:

jshell> 2147483642L + 8
2147483650

jshell> 2000000000 + 2000000000L
4000000000

However, long integers wrap around on overflow, too! Since integer overflow is dangerous (and in real life caused a spaceship to auto destruct) you can use methods that will generate an error immediately rather than giving you wrong answers that will lead to trouble down the line:

jshell> Math.addExact(2147483642, 8)
|  Exception java.lang.ArithmeticException: integer overflow

addExact, subtractExact, and multiplyExact work on both ints and longs.

double and float arithmetic do not wrap around: you just hit Infinity or -Infinity.

jshell> 8.57E200 * 2.99E200
Infinity

What’s the biggest double before Infinity?

jshell> Double.MAX_VALUE
1.7976931348623157E308
Are BigInteger and BigDecimal bounded?

Technically yes, but if you ever hit those bounds you have crossed into the realm of mind-blowing philosophical number theory and ar unlikely to actually be counting anything.

Precision and accuracy

Just like Python’s Float and JavaScript’s number, Java’s double type uses floating-point hardware subject to accuracy and precision limits. You get high precision around 0, and the precision gets lower the farther away you move away from zero. After a while representable numbers get pretty far apart:

jshell> Math.pow(2, 53)
9.007199254740992E15

jshell> Math.pow(2, 53) + 1
9.007199254740992E15

jshell> Math.pow(2, 53) + 2
9.007199254740994E15

Out here, representable numbers are spaced a distance of 2.0 apart. Moving even farther away, you'll find representable numbers spaced apart by thousands or millions. Once you get to the number Java calls Double.MAX_VALUE, the previous value is about 10293 smaller, and the next value is just Infinity.

Just like JavaScript and Python, arithmetic on floating-point values are notoriously subject to roundoff error (a loss of accuracy) even when there is enough precision available! The problem has to do with the hardware being tuned to numbers that are ratios of powers of two: (n/1, n/2, n/4, n/8, n/16, n/32, etc.) Fractions with powers of 10 are no fun; scale up to integers to make your life better:

jshell> 0.1 + 0.2
0.30000000000000004

jshell> (1 + 2) / 10.0
0.3

Sines, cosines, logarithms, and similar operations often have roundoff errors:

jshell> Math.cos(Math.toRadians(90.0))
6.123233995736766E-17

That should have been zero, right? What're ya gonna do?

Understand roundoff error when comparing floating-point numbers.

If x and y are floats or doubles, x == y is true if and only if the values are the same representable number. If doing a computation where roundoff error is possible, just test whether the values are “close enough” to each other, i.e., whether the absolute value of their difference is within a certain tolerance.
How can roundoff error be avoided?

Technically, you can write software to manipulate the individual digits or other components of numbers when extreme or perfect precision and accuracy are demanded. Java’s BigDecimal type (outside the scope of this tutorial) gives the programmer such control.

A warning about division

Please note: if you divide two integers, you do not automatically get a double! Instead you get an integer that is close to the expected result: it rounds toward 0. try the following in the shell:

jshell> 299/100
2

jshell> -299/100
-2

jshell> 299.0/100
2.99

jshell> (double)-299/100
-2.99
Exercise: A common error is to write (double)(-299/100). This does not produce -2.99. What does it produce and why?

A wealth of built-in math operations

Java’s mathematical operations are pretty extensive. Here is a small sampling:

jshell> Math.PI
3.141592653589793

jshell> Math.cos(Math.PI)
-1.0

jshell> Math.hypot(-3, 4)
5.0

jshell> Math.sqrt(5.0625)
2.25

jshell> Math.sqrt(-5.0625)
NaN

jshell> Math.round(93.75)
94

jshell> Math.min(34, -89)
-89

jshell> Math.cosh(3.9188)
25.179932718245226

It is crucial that you browse the full set of Math operations (no time like the present). As a programmer, you will ultimately be responsible for learning and using the built-in operations where possible (but enjoy the learning curve, everyone’s a white belt at something at first).

NaN

Java is just like Python and JavaScript in having that crazy value NaN (Not a Number), which pops up when doing mathematical operations that do not produce an actual real number. (It prints as NaN, but you have to represent it as Double.NaN.) One thing you need to know: NaN is not equal to anything, not even NaN. That is why there’s isNaN.

jshell> 0.0 / 0.0
NaN

jshell> Math.asin(5)
NaN

jshell> Math.sqrt(-100)
NaN

jshell> Double.NaN == Double.NaN
false

jshell> Double.isNaN(Math.asin(5))
true

jshell> Double.isNaN(-18)
false

Booleans

The booleans are true and false. They are the values produced from operators like == (equals), != (not equals), < (less-than), <= (less than or equals), > (greater than), and >= (greater than or equals). Boolean values can be combined with && (AND), || (OR), and ! (NOT):

jshell> 13 == 13
true

jshell> 13 > 8
true

jshell> 13 < 8
false

jshell> 2 < 5 && 9 - 3 > 20000
false

jshell> var found = false
false

jshell> !found
true

jshell> 7 < 30 || 2 < 50
true

Short-circuit operators

The great thing about && and || is that they short-circuit: they don’t evaluate their right operand unless necessary. In particular:

x && y
If x is false, the whole expression is false, you’re done! Otherwise you will have to execute y.
x || y
If x is true, the whole expression is true, you’re done! Otherwise you will have to execute y.

& and | don’t short-circuit; you really don’t need to use them for booleans, but in advanced applications people use them for fancy integer tricks. We are not going to discuss those tricks today.

jshell> 3 > 5 && 8 / 0 == 3
false

jshell> 3 > 5 & 8 / 0 == 3
|  Exception java.lang.ArithmeticException: / by zero

The very awesome conditional operator

Some day, this lovely operator will be your friend:

  x ? y : z

Read this as “if x then y else z.” First Java evaluates x; if x is true, it evaluates and produces y. Otherwise it evaluates and produces z. (See, only one, never both, of x and y are evaluated.)

jshell> 3 > 5 ? 100 : 200
200

jshell> 7 * 5 < 90 ? 500 : 800
500

jshell> var latitude = 34.18
34.18

jshell> var hemisphere = latitude > 0 ? "north" : "south"
"north"
Sometimes you will hear people refer to this operator as “the ternary operator.” They say this because
  • unary operators have one operand,
  • binary operators have two operands, and
  • ternary operators have three operands.
Because the conditional operator is the only Java operator with three operands, some people like to make a big deal of this.

Strings

Java represents textual information with what it calls strings. A string is, pretty much, a sequence of characters. We write them enclosed in quotation marks.

jshell> "No Doubt's first album is really good 💃👍💯"
"No Doubt's first album is really good 💃👍💯"

How do you get a quotation mark in the string? You escape it with a backslash.

jshell> "The word of the day is \"omakase\""
"The word of the day is \"omakase\""

jshell> System.out.println("The word of the day is \"omakase\"")
The word of the day is "omakase"

Okayyyyy but then how to get a backslash in the string?

jshell> System.out.println("This is backslash: \\ and this is forward slash: /")
This is backslash: \ and this is forward slash: /

What about characters that are hard to type or print? We have \t for the tab character, \n for the “new line” (among others):

jshell> System.out.println("Hello\tworld\nHola\tmundo")
Hello	world
Hola	mundo

You can also write multiline characters directly with three quotation marks:

jshell> var poem = """
   ...>     No more water
   ...>       in the bucket
   ...>     No more moon
   ...>       in the water"""
poem ==> "No more water\n  in the bucket\nNo more moon\n  in the water"

jshell> System.out.println(poem)
No more water
  in the bucket
No more moon
  in the water

It’s nice how Java allows you to format the code nicely without having the left margin embedded as spaces in your string!

Basic string operations

What can we do with strings? So many things! Here is a sampler; try them out in JShell:

"".isEmpty()                          // true
"    ".isEmpty()                      // false
"    ".isBlank()                      // true
"    abc   ".stripLeading()           // "abc   "
"    abc   ".stripTrailing()          // "    abc"
"    abc   ".strip()                  // "abc"
"doghouse".startsWith("do")           // true
"doghouse".endsWith("house")          // true
"doghouse".contains("ghou")           // true
"yak".repeat(3)                       // "yakyakyak"
"HELLO".toLowerCase()                 // "hello"
"hello".toUpperCase()                 // "HELLO"
"resume".replace('e', 'é')            // "résumé"
"dog" + "house"                       // "doghouse"

Converting to and from strings:

String.valueOf(809)                   // "809"
String.valueOf(false)                 // "false"
String.valueOf(95.23E-2)              // "0.9523"

Integer.parseInt("950")               // 950
Integer.parseInt("1101", 2)           // 13
Integer.parseInt("ff8a", 16)          // 65418
Integer.parseInt("dog")               // (throws exception)
Double.parseDouble("7222.9118E3")     // 7222911.8

It is crucial that you browse the full set of string operations (no time like the present).

Strings are immutable

Please note: A STRING OBJECT CAN NEVER BE CHANGED! You cannot replace a character in a string object. You cannot insert a character into a string object. You cannot delete a character from a string object. Once a string is made it is set for life. It is immutable.

So wait, what do strip, toLowerCase, replace, and concat do? THEY PRODUCE ENTIRELY NEW STRINGS.

jshell> var message = "Mind the gap"
"Mind the gap"

jshell> message.toUpperCase()
"MIND THE GAP"

jshell> message
"Mind the gap"
Exercise: Please, please, please study that example. Note that the string object bound to the variable message did not change. The expression message.toUpperCase() created a new, different, string. Commit this to memory. Tell a friend about it. And in the future, when you will no doubt run into a bug where you thought you were mutating a string but were not, try to remember this day and fix your bug quickly.

Equality—watch out!

A common mistake Java programmers make is thinking == compares the characters of the strings. It does not. It does something else; what it does need not concern you now. Just. DON’T. USE. IT. FOR. STRINGS. Again: DON’T COMPARE STRINGS WITH ==. (If you want to know if two strings have exactly the same sequence of characters, use .equals:

jshell> var pet = "Sparky"
"Sparky"

jshell> var fren = "s".toUpperCase() + "park".concat("y")
"Sparky"

jshell> pet == fren
false

jshell> pet.equals(fren)
true

Operations involving indexing and string length

Java seems like a typical programming language when it comes to strings: the length is the number of characters, the first index position is 0, and substring operations are inclusive at the beginning and exclusive at the end. Here’s an example string, and some operations on it:

  0   1   2   3   4   5   6   7   8   9
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ П │ р | и | в | е | т |   | м | и | р |
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
jshell> var message = "Привет мир"
"Привет мир"

jshell> message.length()
10

jshell> message.charAt(5)
'т'

jshell> message.substring(3, 8)
"вет м"

jshell> message.substring(3)
"вет мир"

jshell> message.indexOf("м")
7

jshell> message.lastIndexOf("и")
8

jshell> message.indexOf("Z")
-1

But sometimes, things seem not to make sense:

jshell> var s = "👍👩‍🔬$🇺🇾"
"👍👩‍🔬$🇺🇾"

jshell> s.length()
12

jshell> s.charAt(7)
'$'

What?! It turns out that what Java thinks are characters are not characters! To understand strings, we have to understand characters, and why Java is so wrong about all this. It turns out, though, that it takes a long time to truly understand characters—more time than we have for these notes. The next section, though, will give you a very brief introduction: enough to tell you that indexing and length-taking for strings is not easy, but not enough to tell you exactly how to always do it right. In fact there probably is no “right” because not eveyone agrees on what the length of a string actually is.

Characters

In real life, but not in Java: A character is a unit of information in textual data. Every character has a name, for example:

A grapheme is a minimally distinctive unit of writing in some writing system. It is what a person usually thinks of as a character. However, it may take more than one character to make up a grapheme. For example, the grapheme:

is made up of two characters (1) LATIN CAPITAL LETTER R and (2) COMBINING RING ABOVE. The grapheme:

நி

is made up of two characters (1) TAMIL LETTER NA and (2) TAMIL VOWEL SIGN I. This grapheme:

🚴🏾

is made up of two characters (1) BICYCLIST and (2) EMOJI MODIFIER FITZPATRICK TYPE-5. This grapheme:

🏄🏻‍♀

is made up of four characters (1) SURFER, (2) EMOJI MODIFIER FITZPATRICK TYPE-1-2, (3) ZERO-WIDTH JOINER, (4) FEMALE SIGN. And this grapheme:

🇨🇻

requires two characters: (1) REGIONAL INDICATOR SYMBOL LETTER C and (2) REGIONAL INDICATOR SYMBOL LETTER V. It’s the flag for Cape Verde (CV).

Finally, a glyph is a picture of a character (or grapheme). Two or more characters can share the same glyph (e.g. LATIN CAPITAL LETTER A and GREEK CAPITAL LETTER ALPHA), and one character can have many glyphs (think fonts, e.g., A A A $\mathcal{A}$ $\mathscr{A}$).

Exercise: Define the terms character, grapheme, and glyph in your own words.
Exercise: The following three characters can be represented by the same glyph: LATIN SMALL LETTER O WITH STROKE, DIAMETER SIGN, EMPTY SET. Draw such a glyph.
Exercise: What two characters would you use to make the Spanish flag?

Sadly, what Java thinks is a character does not align with any of these terms. If you are lucky, you won’t ever have to care about lengths, or substrings, or the nth character of a string! Just let strings be strings. Java can be very misleading in these areas. Remember our example from the last section:

jshell> "👍👩‍🔬$🇺🇾".length()
12

This string has FOUR graphemes (1. Thumbs up, 2. Woman scientist, 3. dollar sign, 4. Uruguayan flag), but it has SEVEN characters (Remember: characters are textual information units, not written symbols). The seven characters are:

  1. THUMBS UP SIGN
  2. WOMAN
  3. ZERO WIDTH JOINER
  4. MICROSCOPE
  5. DOLLAR SIGN
  6. REGIONAL INDICATOR SYMBOL LETTER U
  7. REGIONAL INDICATOR SYMBOL LETTER Y

But Java says the length is 12 and not 7? Why? Answer: Java chars are NOT characters: they are—get this—UTF-16 code units. WHAT??? Okay, UTF-16 code units are beyond this introductory tutorial, so we won’t go into details, other than to say some some characters require one code unit and some require two, and this is what makes the reported length longer than 7.

You asked for more details, didn’t you?

If so, read this box, but feel free to skip it. Here’s the deal: (1) Every character is assigned a unique code point, an integer between 0 and 1114111). (2) Code points <= 65535 are encoded with just one code unit; all others are encoded in two. (3) To find the code points for each character, and the algorithm for the UTF-16 encoding scheme, do a web search. (4) When you ask Java for the length of a string, it counts code units. Here is the actual, real life break down of the example string above:
GraphemeCharacterCode PointCode Units
Thumbs UpTHUMBS UP SIGN12807755357, 56397
Woman ScientistWOMAN12810555357, 56425
ZERO WIDTH JOINER82058205
MICROSCOPE12830055357, 56620
Dollar SignDOLLAR SIGN3636
Uruguayan FlagREGIONAL INDICATOR SYMBOL LETTER U12748255356, 56826
REGIONAL INDICATOR SYMBOL LETTER Y12748655356, 56830
Super Bonus Advanced Fun Time

Curious how I inspected all that? Try these in JShell:
  "👍👩‍🔬$🇺🇾".codePoints().toArray()
  "👍👩‍🔬$🇺🇾".chars().toArray()
  "👍👩‍🔬$🇺🇾".codePoints().mapToObj(Character::getName).toArray()
Don’t worry too much about that right now. But if you are interested, I have some more detailed notes on characters.
String indexing and length-taking are highly advanced topics

Until you have learned about Unicode, UTF encoding schemes, and Unicode normalization, you shouldn’t really be mucking around with the index positions of individual characters or taking the length of strings.

If you do find yourself interviewing for an internship position and someone asks you a question about indexing and lengths, simply ask your interviewer: “may I ignore astral characters and normalization concerns?” They will probably be impressed. Then you can go ahead and assume one code unit per character.

Objects

Okay, so we covered the fact that Java calls the eight types byte, short, int, long, float, double, and char primitive types. Why “primitive”? Because values of these types are atomic. Strings, on the other hand, are made up of individual characters—they are non-atomic, i.e. decomposable. Java calls them objects.

EVERY VALUE
NOT OF A PRIMITIVE TYPE
IS AN OBJECT

Java comes with other built-in objects: lists, maps (what Python calls dictionaries), sets, queues, processes, threads, string builders, dates, and many, many, more. You will make up more of your own, too, for example:

Objects are (1) made up of named properties called fields (2) are operated on by methods, and (3) are created with constructors. Every object belongs to exactly one class, and to create an object, you use new with the class name. The object is said to be an instance of its class.

jshell> var builder = new StringBuilder(10)

jshell> var enemySpawner = new Timer("Spawn new enemies")
java.util.Timer@238e0d81

jshell> var random = new Random()
java.util.Random@377dca04

jshell> var used = new PriorityQueue<String>()
[]

Some objects print funny, don’t they? By default, objects are displayed (rendered as strings) with their class name and some weird hex numeral, but as we’ll see later, the class designer has control over how this rendering happens. (Note above, the designers of StringBuilder and PriorityQueue took advantage of this capability, but the Random and Timer class designers did not.)

The fields of an object represent the object’s state, or current value. There is a class called Rectangle in the standard Java library package java.awt. Let’s construct a rectangle and then explore its fields in JShell:

jshell> import java.awt.Rectangle

jshell> var r = new Rectangle(5, 8, 20, 10)
java.awt.Rectangle[x=5,y=8,width=20,height=10]

jshell> r.y
8

jshell> r.width = 13
13

jshell> r
java.awt.Rectangle[x=5,y=8,width=13,height=10]

You can see the rectangle object has four fields: x, y, width, and height. The fields are accessed with dot-notation, and updated with =.

The methods of an object carry out computations. Sometimes methods mutate the object, and sometimes they don’t:

jshell> r
java.awt.Rectangle[x=5,y=8,width=13,height=10]

jshell> r.translate(2, -5)

jshell> r
java.awt.Rectangle[x=7,y=3,width=13,height=10]

jshell> r.setSize(100, 22)

jshell> r
java.awt.Rectangle[x=7,y=3,width=100,height=22]

jshell> r.contains(40, 11)
true

jshell> r.contains(6, 20)
false

jshell> r.union(new Rectangle(11, -2, 105, 7))
java.awt.Rectangle[x=7,y=-2,width=109,height=27]

jshell> r
java.awt.Rectangle[x=7,y=3,width=100,height=22]

Note:

Exercise: Study the documentation for this built-in Rectangle class. Identify the constructors, fields, and methods. Identify, for each method, whether or not it is a mutator.
Terminology time

An object whose fields cannot be changed, either by direct assignment or through any method, is called an immutable object. Clearly, rectangles are not immutable. There are many advantages of immutable objects. Still, real-life code has a mix of mutable and immutable objects.

For any given object, what its fields are, and how the constructors and methods work, (and whether the object is mutable or not), are all defined in the object’s class. Java comes with thousands upon thousands of built-in classes. But we need to build our own. Ready?

Classes

The Java standard library have over 4,000 classes, but we want to learn how to build our own. When you build a class, you ask yourself:

  1. What do objects of this class HAVE? (fields)
  2. What do objects of this class DO? (methods)
  3. How should objects of this class be constructed?
  4. Which fields should be mutable (if any)?
  5. What fields and methods should be invisible to the outside?
  6. Should equality be value-based or simply identity-based?
  7. How should values be printed? (because the default is ugly)

As a first example, let’s build a player class. We’ll deal with the first five questions only for now. Players have a name (immutable) and a level (mutable). However, even though the level field is mutable, we don’t want to expose it directly. We’d prefer to tell the player to go up or down, rather than setting the level directly via field access. Accessing an object only through methods give us control and we can do better security.

Player.java
public class Player {
    private static final int MAX_LEVELS_TO_ASCEND = 5;
    private static final int MAX_LEVELS_TO_DESCEND = 3;

    private final String name;
    private int level;

    public Player(String name) {
        this.name = name;
        this.level = 0;
    }

    public String name() {
        return name;
    }

    public int level() {
        return level;
    }

    public void moveUp(int levelsToAscend) {
        if (levelsToAscend < 0 || levelsToAscend > MAX_LEVELS_TO_ASCEND) {
            throw new IllegalArgumentException("Levels to ascend out of range");
        }
        this.level += levelsToAscend;
    }

    public void moveDown(int levelsToDescend) {
        if (levelsToDescend < 0 || levelsToDescend > MAX_LEVELS_TO_DESCEND) {
            throw new IllegalArgumentException("Levels to descend out of range");
        }
        this.level -= levelsToDescend;
    }
}
jshell> /open Player.java

jshell> var p = new Player("Mario")
Player@378bf509

jshell> p.level()
0

jshell> p.moveUp(5)

jshell> p.moveDown(2)

jshell> p.level()
3

In practice, classes come in two varieties: The first are those that make up living, breathing, active objects, like players that have really interesting behaviors, like the ability to jump up and down. The second are plain old passive data objects, that are basically just containers of values. The former tend to be mutable and the latter completely immutable. We generally never care about equality of active objects, but we do for the passive ones.

It turns out equality of objects is a ridiculously complex topic.

Identity versus equality

What happens when we compare objects?

jshell> class Location {
   ...>     double latitude;
   ...>     double longitude;
   ...>     Location(double latitude, double longitude) { 
   ...>         this.latitude = latitude; 
   ...>         this.longitude = longitude;
   ...>     }
   ...> }
|  created class Location

jshell> var p1 = new Location(37.6308, 119.0326)
Location@378bf509

jshell> var p2 = p1;
Location@378bf509

jshell> var p3 = new Location(37.6308, 119.0326);
Location@16b98e56

jshell> p1 == p2
true

jshell> p1 == p3
false

jshell> p1.equals(p2)
true

jshell> p1.equals(p3)
false

What just happened? It turns out primitives and non-primitives in Java have one massive difference. It is a simple notion, but technical, and you only internalize it after some practice. Here goes:

The value of an non-primitive expression is a reference (“pointer”) to an object (not the object itself).

A picture is crucial here, For the code above we get:

3vars2objs.png

IMPORTANT: Note that there are 3 variables here but only 2 objects. Check the code again make sure you understand why.

Now here’s the thing. For objects:

Because pointers “refer to” objects, they are called references. Because all non-primitive types are implemented with references, they are called reference types. All types in Java are either primitive types or reference types. Members of a reference type are called objects. And yes, we did see Java objects earlier.

But what if you wanted to test whether two distinct objects happened to have the same values for all their fields? You can of course write code to check the fields yourself. The convention for programming this test is to house it in a method called equals, which you override in your own class.

What is overriding?

Every object in Java automatically comes with a few methods: equals, hashCode, getClass, toString, clone, wait, notify, and notifyAll. These methods have default behavior that you might want to specialize for your own class. For example, the default equals method works exactly like ==. You might wish to specialize this behavior by overriding it to check values of fields.

The built-in behavior of equals is to just do the same thing as ==, hence the need to override if you want value-based equality rather than a pure identity test. But remember, in practice, you rarely need to do this for active objects. You often do want value-based equality for passive, immutable objects. But guess what? Java helps you here. Use a record to get immutability, value-based equality, and more!

Records

If you would like to make a class that is fully immutable, with equality based on the values of all the fields, you can use the record syntax:

jshell> record Location(double latitude, double longitude) {}
|  created record Location

jshell> var p1 = new Location(37.6308, 119.0326)
Location[latitude=37.6308, longitude=119.0326]

jshell> var p2 = new Location(37.6308, 119.0326)
Location[latitude=37.6308, longitude=119.0326]

jshell> p1.equals(p2)
true

jshell> p1.latitude
|  Error:
|  latitude has private access in Location
|  p1.latitude
|  ^---------^

jshell> p1.latitude()
37.6308

NICE! A record automatically makes an immutable class! It:

If you need to add validation to the constructor, or add in some extra methods, you can:

Location.java
public record Location(double latitude, double longitude) {
    public Location {
        if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) {
            throw new IllegalArgumentException("Value out of range");
        }
    }

    public boolean inNorthernHemisphere() {
        return latitude > 0;
    }
}

Records Behind the Scenes

Records are just classes. You should use them as often as possible, because immutability is awesome. But, to be a good Java programmer, you should know what goes on behind the scenes. Records give you immutability for free, method accessors for free, and a proper value-based equality method. Doing all those things yourself is surprisingly really hard! Value-based equality is super hard, because:

  • There are a ton of restrictions placed on how the equals method is supposed to behave, seemingly the least of which is the fact that Java requires that the thing you compare to can belong to any class.
  • For very technical reasons outside the scope of this tutorial, you should NEVER override equals without overriding hashCode.

Yikes. The details are just too technical at this point. So we’ll just present some code and study it in class:

// Don't write this yourself, we're only showing what the record produces.

import java.util.Objects;

public class Point {
    public static final Point ORIGIN = new Point(0, 0);

    private final double x;
    private final double y;

    public Point(double x, double y) {
        if (Double.isNaN(x) || Double.isNaN(y)) {
            throw new IllegalArgumentException("Coordinates can not be NaN");
        }
        this.x = x;
        this.y = y;
    }

    public double x() {
        return x;
    }

    public double y() {
        return y;
    }

    public double distanceFromOrigin() {
        return Math.hypot(x, y);
    }

    public Point reflectionAboutOrigin() {
        return new Point(-x, -y);
    }

    public static Point midpointOf(Point p, Point q) {
        return new Point((p.x + q.x) / 2.0, (p.y + q.y) / 2.0);
    }

    @Override
    public boolean equals(Object o) {
        return (o instanceof Point other) && x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

Yay, new Java features were introduced here! We’ll go over the example in class in some detail, but first, let’s use the class to get a sense of things. Put the file Point.java in the same folder in which you launched jshell. Then follow along:

jshell> /open Point.java

jshell> var p = new Point(3, 8)
(3.0, 8.0)

jshell> p.distanceFromOrigin()
8.54400374531753

jshell> Point.midpointOf(new Point(1,1), new Point(3,3))
(2.0, 2.0)

jshell> var q = new Point(3, 8)
(3.0, 8.0)

jshell> Map.of(new Point(1, 1), "dog", new Point(1, 1), "rat")
|  Exception java.lang.IllegalArgumentException: duplicate key: (1.0, 1.0)

Now let’s break down the code and learn some new things:

  • The class has three fields, but one of them is marked static. Non-static fields belong to the instances and are called instance fields; static fields belong to the class and are called class fields. Instances created with new Point have their own x and y fields; the static field ORIGIN belongs to the class and is accessed like this: Point.ORIGIN.
  • The midpointOf method is marked static, so it is called as Point.midpointOf(p, q); that is, it is called on the class, not on any instance. It is called a class method. The other methods are all instance methods.
  • We are following Java convention and making all of our instance fields private. (The rectangle class we saw earlier is a bit of an anomaly.) Details about access modifiers and more are in a separate page of notes. You can also read about them in the Java Language Specification.
  • Because the equals, hashCode, and toString methods already have default implementations for all classes, we mark them with the @Override annotation when defining our customized behaviors.
  • The equals method is designed to be used as p.equals(q) where q is any object whatsoever, and not necessarily another Point object. This means to implement properly, you must check that this other object is a Point. The instanceof operator will perform this test and if it passes, give you a new properly type-constrained Point variable for you to begin checking the equality of each corresponding field.
  • For technical reasons, whenever you override equals you have to override hasCode. Fortunately this is easy: pass the fields you care about to Objects.hash and you are good to go.
  • In case you are dying of curiosity about this hashCode thing, we’ll just say this for now: if you don’t override hashCode then points won’t work as dictionary keys or set members the way you expect.
You don’t HAVE to override equals and hashCode

It’s likely that for most classes you will ever write, you won’t ever have to compare them at all. But you DO need to know that if you do, == will check identity only, so in those cases where to want field-based equality, you have to walk the walk and override both.

Exercise: Build a class for dresses with a price, brand, and size, along the lines of the example Point class above. Create two different dress instances.
There is so much more to classes.

As this is only an introductory tutorial, we barely scratched the surface of classes, but that’s enough for now. If interested, see this page of notes discussing Java classes in much more detail.

Understanding null

The value null (a.k.a. the null reference) is a member of every reference type. It’s annoying, dangerous, and error-prone. It’s inventor, Tony Hoare, calls it his Billion-Dollar Mistake because of the havoc it has wrought over the years. But it is an inescapable fact of life in Java. You cannot avoid it. So you have to understand it. The first thing to understand is: null does not have any fields or methods. Any attempt to dereference null throws a NullPointerException.

jshell> var message = "Stay behind the yellow line"
"Stay behind the yellow line"

jshell> message.toUpperCase()
"STAY BEHIND THE YELLOW LINE"

jshell> message = null
null

jshell> message.toUpperCase()
|  Exception java.lang.NullPointerException

So what does this mean? It means whenever you use a dot, as in the expression r.x (where r is a reference and x is a field or method), ask yourself “Is there any way r can be null here?” Try to make sure your code never will! If you can’t avoid it, you need to probably make a check:

if (r != null) {
    doSomethingWith(r.x)
}
Why do we have null?

It’s supposed to stand for “no value here.” For example:
    var supervisor = new Supervisor("Tina", ...);
means you have a supervisor named Tina, whereas
    var supervisor = null;
means you don’t have a supervisor. The problem is that the expression (for example):
    supervisor.performPerformanceReview(employee)
can throw a NullPointerException. You can get around all this by using the class Optional. We’re not going to cover it in these notes. It’s a huge improvement, but you have to go through a learning curve and you have to use it properly. It doesn’t solve all your problems, but it’s good to know about, at least.

Now you know what public static void main is

In Java, all your code has to be in some class! So if you just want to write a simple command line application, you need a class. But you don't want to create an object just to run your code, so...we use a static method! The method needs to be public so it can be seen from outside the class, and is marked void because it does not return anything. The command line arguments are all rolled into an array, so there you go:

Greeter.java
public class Greeter {
    public static void main(String[] args) {
        System.out.print("Hello there");
        if (args.length > 0) {
            System.out.print(", " + args[0]);
        }
        System.out.println(".");
    }
}

Classes in the standard library

Your Java implementation comes with thousands upon thousands of built-in classes. Classes are grouped into packages, so every class has a fully qualified name such as java.time.LocalDate, java.awt.Rectangle, or javax.crypto.KeyGenerator.

If you don’t want to use the full class name, you can import the class at the top of your program, or in JShell. (One exception: you don’t have to qualify classes from java.lang.)

NewYearCountdown.java
public class NewYearCountdown {
    public static void main(String[] args) {
        var today = java.time.LocalDate.now();
        var nextNewYearsDay = java.time.LocalDate.of(today.getYear() + 1, 1, 1);
        var days = java.time.temporal.ChronoUnit.DAYS.between(today, nextNewYearsDay);
        System.out.println("Hello, today is " + today);
        System.out.printf("There are %d days until New Year’s Day%n", days);
    }
}
$ javac NewYearCountdown.java && java NewYearCountdown
Hello, today is 2020-01-09
There are 358 days until New Year’s Day
Fun tip

JShell actually imports hundreds of frequently used classes to be nice to you, but far from all of them, so be ready to do a lot of importing when needed.

Enums

An enum is a simple kind of class, one whose instances are fixed once and for all. You can never create another instance of the type. Examples:

jshell> enum TrafficLightColor { RED, AMBER, GREEN }
|  created enum TrafficLightColor

jshell> enum Direction { NORTH, EAST, SOUTH, WEST }
|  created enum Direction

jshell> var stopColor = TrafficLightColor.RED
RED

jshell> stopColor
RED

jshell> System.out.println(stopColor)
RED

jshell> TrafficLightColor.valueOf("GREEN")
GREEN

Like classes, the values of an enum type can have properties and behaviors attached to them. Perhaps the best way to see how this works is to study this modification of the Enum example from the Oracle’s Java™ Tutorials:

Planet.java
public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    public final double mass;   // in kilograms
    public final double radius; // in meters

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    double surfaceGravity() {
        // G is the universal gravitational constant (m³ / kg / s²)
        double G = 6.67300E-11;
        return G * mass / (radius * radius);
    }

    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }

    public static void main(String[] args) {
        for (var p: Planet.values()) {
            System.out.printf("Radius of %s ≈ %.0f m%n", p, p.radius);
            System.out.printf("Mass of %s ≈ %.3e kg%n", p, p.mass);
            System.out.printf("Surface gravity of %s ≈ %f m/s²%n", p, p.surfaceGravity());
            System.out.printf("100kg on %s weighs %g kg•m/s²%n", p, p.surfaceWeight(100.0));
            System.out.println();
        }
    }
}

The public static void main is only there to illustrate how enums are used. How convenient:

$ javac Planet.java && java Planet
Radius of MERCURY ≈ 2439700 m
Mass of MERCURY ≈ 3.303e+23 kg
Surface gravity of MERCURY ≈ 3.703027 m/s²
100kg on MERCURY weighs 370.303 kg•m/s²

Radius of VENUS ≈ 6051800 m
Mass of VENUS ≈ 4.869e+24 kg
Surface gravity of VENUS ≈ 8.871392 m/s²
100kg on VENUS weighs 887.139 kg•m/s²

.
.
.
Radius of NEPTUNE ≈ 24746000 m
Mass of NEPTUNE ≈ 1.024e+26 kg
Surface gravity of NEPTUNE ≈ 11.158635 m/s²
100kg on NEPTUNE weighs 1115.86 kg•m/s²

Let’s unpack a bit:

  • enum Planet declares a new enum type called Planet
  • There are exactly eight instances of the type, MERCURY through NEPTUNE.
  • Each of the instances have two fields (mass and radius), used, for example, as follows: MARS.mass and SATURN.radius.
  • Each of the instances have two methods (surfaceGravity and surfaceWeight), used, for example, as follows: EARTH.surfaceGravity() and VENUS.surfaceWeight(7.95).
  • Enums have constructors, just as classes do.
  • We threw in a main for simplicity; real classes hardly ever have one. Our main method does illustrate one cool thing about enums: the static values method returns an array of the instances of the type. Nice for iteration!

Interfaces

When multiple classes have overlapping sets of methods, but implement them in a slightly different way, interfaces can be incredibly useful. Just a quick example for now: a two-dimensional shape is something for which you can compute an area and a perimeter. It makes sense, then, to talk about lists of shapes and operate on those lists, often without worrying about the specific type of shape.

interface Shape2d {
    double perimeter();
    double area();
}

record Circle(double radius) implements Shape2d {
    public double perimeter() { return 2 * Math.PI * radius(); }
    public double area() { return Math.PI * radius() * radius(); }
}

record Rectangle(double width, double height) implements Shape2d {
    public double perimeter() { return 2 * (height() + width()); }
    public double area() { return width() * height(); }
}

Here’s an operation that operates on entities “through their interfaces”:

var sum = 0.0;
for (var shape: List.of(new Circle(3), new Rectangle(2.2, 8.88), new Circle(5.13))) {
    sum += shape.area();
}

Note that you can have variables, fields, and parameters of an interface type:

class Player {
    String name;
    Shape2d favoriteShape;
}

Shape s1 = new Circle(1.0);
Shape s2 = new Rectangle(2.0, 2.0);

double halfwayAround(Shape2d s) { return s.perimeter() / 2.0; }

While an interface is indeed a type, it is not something you can instantiate:

jshell> var s = new Shape2d()
|  Error:
|  Shape2d is abstract; cannot be instantiated
|  var s = new Shape2d();

Statements

Expressions are evaluated to produce results. Statements are executed to perform actions. In Java, statements always appear inside the body of a method.

Simple Statements

Here is an example program that illustrates five kinds of simple statements inside the main method of a small application (by “simple” we mean statements that aren’t made up of other statements):

SimpleStatementDemo.java
public class SimpleStatementDemo {
    public static void main(String[] args) {
        var happiness = 7;               // Local variable declaration
        happiness = happiness + 2;       // Assignment statement
        happiness++;                     // IncrementOrDecrement statement
        ;                                // Empty statement
        System.out.println(happiness);   // Method invocation
    }
}
Exercise: Run the program. What did it output?

Note the difference between a variable declaration and an assignment. The former brings a variable into existence; the latter updates the value of an already-existing variable.

Conditionals

The If Statement

The if statement selects a computation by examining a sequence of conditions until it finds one that is true:

if (prohibitedFromVoting) {
    System.out.println("Sorry, you can't vote");
} else if (age >= 19) {
    System.out.println("I hope you are a regular voter");
} else if (age >= 18) {
    System.out.println("Congratulations, new voter");
} else {
    var yearsToWait = 18 - age;
    System.out.printf("You can vote in " + yearsToWait + " years");
}

The Switch Statement

A switch statement can be used when all the choices are based on the value of a single expression (which must be a char, byte, short, int, Character, Byte, Short, Integer, String, or of an enum type):

switch (direction) {
    case "North" -> row--;
    case "East" -> column++;
    case "West" -> column--;
    case "South" -> row++;
    default -> throw new IllegalArgumentException(
            "You should have used an enum!");
}
Avoid the legacy switch

Java supports the ugly version of the switch that C, C++, and JavaScript have. This version uses a : instead of -> and each switch block (the part after the case) is a sequence of statements, which “fall through” to the next case unless a break is present. There is no reason to mess with this version at all. It was created in the ancient period of programming languages where folks were looking for expressive ways to take advantage of the hardware of the day.

Alternatives to If and Switch Statements

Be careful not to overuse if and switch statements. Quite frequently the expression forms of each are more appropriate than the statement forms. So replace this kind of if statement:

// THIS CODE IS POOR: IT EMPHASIZES THE "IF"
String hemisphere;
if (latitude >= 0) {
    hemisphere = "north";
} else {
    hemisphere = "south";
}

with the conditional expression:

// THIS CODE IS PROPER: IT EMPHASIZES THE ASSIGNMENT
var hemisphere = latitude >= 0 ? "north" : "south";

Switches can be used as expressions, too:

var word = switch (amount) {
    case 1 -> "single";
    case 2 -> "pair";
    case 3 -> "trio";
    case 12 -> "dozen";
    case 4, 5, 6 -> "few";
    default -> String.valueOf(amount);
};

And never use if statements for simple “lookups”!

var state = "NSW"; // (for example)

// THIS CODE IS VERY BAD, THERE IS TOO MUCH REPETITION
String capital;
if (state == "SA") {
    capital = "Adelaide";
} else if (state == "QLD") {
    capital = "Brisbane";
} else if (state == "ACT") {
    capital = "Canberra";
} else if (state == "NT") {
    capital = "Darwin";
} else if (state == "TAS") {
    capital = "Hobart";
} else if (state == "VIC") {
    capital = "Melbourne";
} else if (state == "WA") {
    capital = "Perth";
} else if (state == "NSW") {
    capital = "Sydney";
} else {
    throw new IllegalArgumentException("No such state or territory");
}
System.out.println(capital);

You should use maps. This is what maps are for. You should focus on the the data:

// THIS CODE IS BEAUTIFUL
var capitals = Map.of(
    "SA", "Adelaide",
    "QLD", "Brisbane",
    "ACT", "Canberra",
    "NT", "Darwin",
    "TAS", "Hobart",
    "VIC", "Melbourne",
    "WA", "Perth",
    "NSW", "Sydney"
);
System.out.println(capitals.get(state));

Perhaps you cooooooooouuuuullllllddddd use a switch expression for this:

// THIS CODE IS OK BUT THE MAP IS PROBABLY BETTER
var capital = switch (state) {
    case "SA" -> "Adelaide";
    case "QLD" -> "Brisbane";
    case "ACT" -> "Canberra";
    case "NT" -> "Darwin";
    case "TAS" -> "Hobart";
    case "VIC" -> "Melbourne";
    case "WA" -> "Perth";
    case "NSW" -> "Sydney";
    default -> throw new NoSuchElementException();
};
Exercise: Okay, here’s your chance. Have a vigorous but friendly debate as to whether the map or the switch expression is better. Maybe one is better in some contexts but not others? Think up many scenarios.
Exercise: Rewrite both the map version and the switch expression version assuming that the Australian states was an Enum type.
Exercise: Actually, the example above, while inclusive of our fellow humans in the southern hemisphere, was still pretty contrived. How might you actually store the capital of each state in a real application?

You can also take advantage of the short-circuit behavior of && and || for conditional computation. Suppose you were programming a robot and you created the methods swim, run, bike, goThroughDoor, and goThroughWindow, each of which did its thing and returned a boolean value saying whether the task succeeded or not. Now you can write some nice clean expressions:

// The robot won’t bike unless the swim succeeded, and won’t run unless the bike succeeded
var finishedTriathlon = swim() && bike() && run();

// The robot will only try the window if going through the door failed
var gotIn = goThroughDoor() || goThroughWindow();

Iteration

Java has a few statements for doing something repeatedly.

The For-Statement (Definite Iteration)

It you know exactly what you be iterating through (e.g., a range of numbers, the elements of a list, the characters of string), use the famous for loop. Examples only:

jshell> for (var x: List.of(3, 2, 1)) {
   ...>     System.out.println("Count is " + x);
   ...> }
Count is 3
Count is 2
Count is 1

jshell> for (var c: "Hello".toCharArray()) {
   ...>     System.out.println(Character.toUpperCase(c));
   ...> }
H
E
L
L
O

There is a powerful form of the for-statement that is normally used to numeric ranges:

    for ( before ; shouldIKeepGoing ; atEndOfEachIteration)

The before part is done once, then as long as the middle part is true, the body is executed and the last part happens (which sets you up for the next iteration). As a trivial example, here’s counting by 5s:

for (var i = 5; i <= 100; i += 5) {
    System.out.println(i);
}
Exercise: Before running this code, think about what you expect it to do. Then run the code and see if you were right.

It’s not uncommon to see nested for loops. Here is a multiplication table application. It introduces some new features, so run the application yourself and be ready to discuss it with friends in class.

MultiplicationTableApp.java
class MultiplicationTableApp {

    private static final int MIN_SIZE = 1;
    private static final int MAX_SIZE = 20;

    private static final String ARGUMENT_COUNT_ERROR = "Exactly one command line argument required";
    private static final String ARGUMENT_FORMAT_ERROR = "Argument must be an integer";
    private static final String SIZE_ERROR = String.format(
            "Size must be between %d and %d", MIN_SIZE, MAX_SIZE);

    private static void printTable(int size) {
        if (size < MIN_SIZE || size > MAX_SIZE) {
            throw new IllegalArgumentException(SIZE_ERROR);
        }
        for (var i = 1; i <= size; i++) {
            for (var j = 1; j <= size; j++) {
                System.out.printf("%4d", i * j);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        try {
            if (args.length != 1) {
                throw new IllegalArgumentException(ARGUMENT_COUNT_ERROR);
            }
            printTable(Integer.parseInt(args[0]));
        } catch (NumberFormatException e) {
            System.err.println(ARGUMENT_FORMAT_ERROR);
        } catch (IllegalArgumentException e) {
            System.err.println(e.getLocalizedMessage());
        }
    }
}
Exercise: Compile and run this application. Try it with an without command line arguments. Supply “wrong” arguments. Supply good arguments. Explain how the program works.

The While-Statement (Indefinite Iteration)

When you don’t have a definite collection of things to iterate through, and you just need to keep going as long as some condition holds, use the while statement. Here’s an example. A player starts at level 5 and needs to get to level 90. At each “turn” the player moves a randomly-generated number of levels, between -3 and 7. If the player reaches (or goes below) zero, they lose. If they reach (or exceed) level 90, they win. Here’s a complete, runnable program. (Super bonus fun time: it introduces random numbers!)

NonInteractiveLevelGame.java
import java.util.Random;

public class NonInteractiveLevelGame {

    private static int MIN_LEVEL = 0;
    private static int MAX_LEVEL = 90;

    public static void main(String[] args) {
        var random = new Random();
        var level = 5;
        System.out.println("Beginning on level " + level);

        while (level > MIN_LEVEL && level < MAX_LEVEL) {
            var steps = random.nextInt(11) - 3;
            level += steps;
            System.out.println("Took " + steps + " steps, now on level " + level);
        }
        if (level <= MIN_LEVEL) {
            System.out.println("Sorry you lost");
        } else {
            System.out.println("Congratulations!");
        }
    }
}
Exercise: Run the game a few times. Try explaining how it works to friends. (Why explain? “Code Reads” show up a lot in job interviews, so practice!)

Alternatives to the for and while statements

As you gain experience as a programmer, you will learn to use software libraries that contain plenty of operations that hide a lot of looping operations so you don’t have to write loops yourself. For example, in Java, there are built-in operations to:

  • Apply an operation to every element in a collection
  • Find the first (or last) element in a collection that satisfies a property
  • Keep all the elements in a collection that satisfy a property and discard the rest
  • Find the minimum, maximum, sum, average, or median of all the elements in a collection
  • ...and many more
But don’t worry if you don’t know this yet

Mastering programming is a journey. No one begins their career as an expert. It’s okay to write your own loops to do routine tasks while you are learning!

Disruption

Statements are normally executed one after the other, but there are four ways to disrupt the flow. In practice they are almost always embedded in an if statement (do you see why?):

A return statement immediately exits the current method. If the method is not marked void, it will return something.

boolean isPrime(int n) {
    if (n < 2) {
        return false;
    } else if (n == 2 || n == 3) {
        return true;
    } else if (n % 2 == 0 || n % 3 == 0) {
        return false;
    }
    for (int k = 5, w = 2; k * k <= n; k += w, w = 6 - w) {
        if (n % k == 0) {
            return false;
        }
    }
    return true;
}

A break statement immediately exits an entire loop statement, abandoning the current and all future iterations. A continue statement immediately abandons the current iteration (only) and continues with the next iteration.

GuessingGame.java
import java.util.Random;

public class GuessingGame {
    public static void main(String[] args) {
        var secret = new Random().nextInt(100) + 1;
        var message = "Welcome";

        while (true) {
            var input = System.console().readLine(message + ". Guess a number: ");
            int guess;

            try {
                guess = Integer.parseInt(input);
            } catch (NumberFormatException e) {
                message = "Not an integer";
                continue;
            }

            if (guess < secret) {
                message = "Too low";
            } else if (guess > secret) {
                message = "Too high";
            } else {
                System.out.println("YOU WIN!");
                break;
            }
        }
    }
}
$ javac GuessingGame.java && java GuessingGame
Welcome. Guess a number: fifty
Not an integer. Guess a number: 50
Too low. Guess a number: 75
Too low. Guess a number: 89
Too high. Guess a number: 83
Too high. Guess a number: 80
Too low. Guess a number: 82
YOU WIN!

A throw statement abandons the rest of its block, throwing an exception. We’ve seen this statement used a few times above! (Go back and take a look.) We’ll see more about exceptions later.

Summary of Java statements

Here is a complete list of Java statements. We’ve discussed most of them already, but don’t have the space in this introductory tutorial for all of them. Still, this table should be useful to review the ones we’ve seen and get a sneak peek into the others. (If interested you may wish to read about all the statements in the official Java Language Specification.)

Type of StatementStatement TypeWhen to Use
SimpleEmptyYou want to do nothing
Local Variable DeclarationTo bring a new variable into existence
AssignmentTo update the value in a variable
Increment or DecrementTo update the value in a variable by adding or subtracting 1
Method InvocationTo invoke a method just for its side effects
ConditionalIfTo do (zero or) one of a variety of different things, based on completely arbitrary conditions.
SwitchTo do (zero or) one of a variety of different things, based on the value of a single expression
IterationForTo iterate through a fixed range or collection
WhileTo iterate while some condition holds (the condition is checked before each iteration)
Do-whileTo iterate while some condition holds (the condition is checked after each iteration)
DisruptionBreakTo immediately terminate an entire loop
ContinueTo immediately terminate the current iteration of a loop
ReturnTo immediately return from a method
ThrowTo immediately exit the current try-block or method with an error
OtherBlockTo group a bunch of statements together so local variable declarations can have smaller scope.
LabeledTo give a name to a statement, either as documentation or to serve as a target of a break or continue.
SynchronizedTo ensure some code can be executed only by one thread at a time
TryTo define a small section of code for error-handling or resource management
Local Class DeclarationTo make a class used within the current block only
Instance CreationTo create an object without assigning it to a variable. Interestingly, this is sometimes useful: sometimes an object’s constructor will have side effects (like adding the newly created object to a global list)

Arrays

An array is a fixed-size, mutatble, ordered sequence of values. Internally, the elements are packed together tightly in memory making it blindingly fast to access by position. The index of the first element of the array is 0. The length of the array is the number of elements.

  0   1   2   3   4   5   6    7    8
┌───┬───┬───┬───┬───┬───┬───┬────┬────┐
│ 0 │ 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
└───┴───┴───┴───┴───┴───┴───┴────┴────┘

Arrays in Java can never grow or shrink! They can, however, have individual elements updated (they are mutable). Array expressions are normally tagged with the type of their elements:

jshell> var foods = new String[]{"kefir", "zhoug", "orange juice", "lemons"}
String[4]{"kefir", "zhoug", "orange juice", "lemons"}

jshell> foods.length
4

jshell> foods[0]
"kefir"

jshell> foods[3]
"lemons"

jshell> foods[4]
|  Exception java.lang.ArrayIndexOutOfBoundsException

jshell> foods[-1]
|  Exception java.lang.ArrayIndexOutOfBoundsException

jshell> foods[2] = "barazek"

jshell> foods
String[4] { "kefir", "zhoug", "barazek", "lemons" }

There’s one exception to the rule of having to tag arrays with their element type: if you declare an array variable with the type (instead of using var) you can leave off the tag in the initializer:

jshell> int[] picks = {5, 50, 2, 8, 13, 21}

Simple array operations

Let’s get started with copying and filling and printing. In Java, arrays are objects, so = will only copy pointers, not array elements; use copyOf to copy the elements. For printing, use Arrays.toString for arrays of primitives, and Arrays.deepToString for arrays of objects:

jshell> import java.util.Arrays

jshell> var frens = new String[]{"Ratty", "Mole", "Badger", "Toad", "Otter", "Pan"}
String[6]{"Ratty", "Mole", "Badger", "Toad", "Otter", "Pan"}

jshell> Arrays.copyOf(frens, frens.length)
String[6] { "Ratty", "Mole", "Badger", "Toad", "Otter", "Pan" }

jshell> Arrays.copyOfRange(frens, 2, 5)
String[3] { "Badger", "Toad", "Otter" }

jshell> var ones = new int[10]; Arrays.fill(ones, 1)

jshell> ones
int[10] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }

jshell> System.out.println(ones)
[I@45283ce2

jshell> System.out.println(Arrays.toString(ones))
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

jshell> System.out.println(frens)
[Ljava.lang.String;@4629104a

jshell> System.out.println(Arrays.toString(frens))
[Ratty, Mole, Badger, Toad, Otter, Pan]

jshell> int[][] matrix = {{10, 20, 30}, {5, 1, 8}}

jshell> System.out.println(matrix)
[[I@6d21714c

jshell> System.out.println(Arrays.toString(matrix))
[[I@4ccabbaa, [I@4bf558aa]

jshell> System.out.println(Arrays.deepToString(matrix))
[[10, 20, 30], [5, 1, 8]]
Exercise: What is the value of b after the statements var a = new int[]{1, 2, 3}; var b = a; a[1] = 100;? Why?

Array equality—be careful!

Since arrays are objects, the == operator on arrays will only compare identities, not array elements. Use Arrays.equals for arrays of primitives, and Arrays.deepEquals for arrays of objects:

jshell> var a = new int[]{10, 300, 99, -987}

jshell> var b = new int[]{10, 300, 99, -987}

jshell> a == b
false

jshell> Arrays.equals(a, b)
true

jshell> int[][] matrix1 = {{5, 13}, {8, 2}}

jshell> int[][] matrix2 = {{5, 13}, {8, 2}}

jshell> Arrays.equals(matrix1, matrix2)
false

jshell> Arrays.deepEquals(matrix1, matrix2)
true
Wait why not a.equals(b)?

I dont know. That doesn’t work for arrays. Use Arrays.equals(a, b) or Arrays.deepEquals(a, b) instead. I guess arrays are special.

Splitting and joining

You can split a string into a string array, and join the elements of a string array into a big string.

jshell> var userRecord = "255:jdoe:Jane Doe:admin:g727gfrwyVDTQRugP12CFvbsjdv29m"

jshell> userRecord.split(":")
String[5] { "255", "jdoe", "Jane Doe", "admin", "g727gfrwyVDTQRugP12CFvbsjdv29m" }

jshell> var path = new String[]{"up", "right", "right", "down", "left"}

jshell> String.join("->", path)
"up->right->right->down->left"

Sorting

If you just need to sort an array of numbers, the plain old Arrays.sort is fine:

jshell> var picks = new int[]{34, 8, 21, 5, 1, 2}
int[6] { 34, 8, 21, 5, 1, 2 }

jshell> Arrays.sort(picks)

jshell> picks
int[6] { 1, 2, 5, 8, 21, 34 }
Array sortiing is destructive

it sorts “in place” rather than returning a new sorted array.

Why do we sort? Sorting is useful for people reading reports. After sorting, it’s easier to remove duplicates. And perhaps most importantly, sorting makes it easier to find things. If your array is sorted, you can use Arrays.binarySearch, which will tell you, very efficiently, whether a particular value is in your array, and if so where, and if not, where would it go.

Exercise: Read the documentation for binarySearch and experiment with it in JShell.

Summing an array of numbers

Beyond splitting, joining, searching, and sorting, most operations on arrays require you first turn them into a stream. There are historical reasons for this weirdness we can get into later; but for now, let’s just look at the simplest example: summing a numeric array.

jshell> var scores = new int[]{80, 20, 19, -28, 12}
int[5] { 80, 20, 19, -28, 12 }

jshell> Arrays.stream(scores).sum()
103

jshell> Arrays.stream(new int[]{80, 20, 19, -28, 12}).sum()
103

jshell> Arrays.stream(new int[]{}).sum()
0

Mapping and Filtering

Mapping an array is applying an operation to each element in the array, producing an array of the results. Filtering is when you make a new array by just keeping certain elements of the original array. Again, this only works on streams, so we make a stream, do the operations, and pull the stream elements out as a new array:

jshell> var words = "Because I could not stop for Death".split(" ")
String[7] { "Because", "I", "could", "not", "stop", "for", "Death" }

jshell> Arrays.stream(words).filter(
   ...>   s -> s.length() > 3).map(
   ...>   s -> s.toLowerCase()).toArray()
Object[4] { "because", "could", "stop", "death" }
What is the deal with these streams?

Are you wondering why you can’t just map, filter, sum, min, max, average, etc. directly on arrays? Why the streams?

Believe it or not, streams are a good thing! Arrays are not the only kind of sequence! There are also lists, files in a folder, lines of text from a file, sequences of values from ranges, etc. etc. Java has ways to make streams from each of these things. And since all the fancy operations are defined on streams, voilá: they now apply to all these data sources, provided there’s a way to make a stream from them.

And there’s more: the Java Virtual Machine (the process than runs Java programs) can actually figure out internally the best ways to optimize stream operations. Also, wiring together stream pipelines is pretty fun, once you get the hang of it.

Oh, and stream operations are what data scientists do a lot, and data scientists are highly paid. 💰

Varargs

Arrays are by design extremely primitive, with a rather ugly syntax and some design flaws (such as covariance and the need for run-time type checking which you will learn about some day in a programming languages course). There is one very cool Java feature, though, that mitigates some of the syntactic ugliness (yay!): if the last parameter to a method is an array, you can arrange to just call the function with the arguments unpacked:

double average(double... a) {
    // Inside the method, a is an array of doubles
    if (a.length == 0) {
        return Double.NaN;
    }
    var sum = 0.0;
    for (var x: a) {
        sum += x;
    }
    return sum / a.length;
}

// When called, the arguments will be packed into an array
average(2, 92.05, -29, 11.7, 22.3)   // 19.81
average()                            // NaN
average(99)                          // 99.0
average(5, 13)                       // 9.0

This language feature is called varargs since we’ve essentially made an operation that takes “a variable number of arguments.”

Why are varargs cool? The alternative would be to declare the function as double average(double[] a) and call it with average(new double[]{5, 10, 20, -15} (with the ugly type prefix).

Exercise: See if the Java compiler accepts public static void main(String... args) in an application.

Lists

Java has another kind of sequence, called the list. How do lists differ from arrays?

Java ArraysJava Lists
Always have fixed sizeYou can have fixed size or variable size lists
Always mutableYou can have mutable or immutable lists
Elements are always packed together in memoryCan be implemented with elements tightly packed (for efficient access by index) or with links (for efficient insertion and deletion)
Can contain primitives or objectsCan contain objects only 🤦‍♀️🤦‍♀️🤦‍♀️
Covariant 🤦‍♀️🤦‍♀️🤦‍♀️Invariant

Don’t panic, you don’t have to know what covariant and invariant are today
Very small number of built-in operations (for example no indexOf and no swap)Huge (really huge) set of built-in operations
a.equals(b) DOES NOT give you element-by-element equals, you must use Arrays.equals(a,b) insteada.equals(b) does element-by-element equality just fine
Compile-time (static) typechecking can not catch all type errors! Typechecking has to take place at runtime (dynamic) 🤦‍♀️🤦‍♀️🤦‍♀️Compile-time type safety is guaranteed, no need for run-time typechecking
Designed for low-level operations, “behind-the-scenes” and system-level stuff, where performance is critical; not for general day-to-day programmingThe sequence type you will use 99% of the time (only go down to arrays in rare cases)

Here are a couple ways to create lists. Note the List.of syntax gives you a fixed-size list, but the new ArrayList form allows for growth:

jshell> var frens = List.of("Kim", "Ron", "Wade")
[Kim, Ron, Wade]

jshell> frens.add("Bonnie")
|  Exception java.lang.UnsupportedOperationException

jshell> var frens = new ArrayList<String>(List.of("Kim", "Ron", "Wade"))
[Kim, Ron, Wade]

jshell> frens.add("Monique")
true

jshell> frens
[Kim, Ron, Wade, Monique]

There are quite a few ways to make lists; Java is so flexible. For reference, here are a few examples:

CodeGrow / shrink?Mutable?Explanation
List.of(1, 2, 3) No No Fixed-size, immutable list of exactly the elements you specify
List.copyOf(collection) No No Fixed-size, immutable list made from copying the elements of another collection, in order. A collection in Java can be a list, set, dequeue, priority queue, or quite a few other things.
Collections.emptyList() No No Fixed-size, immutable, empty list
Collections.singletonList(x) No No Fixed-size, immutable list of with one element, x
Collections.nCopies(n, x) No No Fixed-size, immutable list of n copies of x
Arrays.asList(1, 2, 3) No Yes Fixed-size, mutable list of exactly the elements you specify
new ArrayList<T>() Yes Yes Initially empty, variable-sized, mutable list whose elements have type T, good for indexing by position
new LinkedList<T>() Yes Yes Initially empty, variable-sized, mutable list whose elements have type T, good for inserting or deleting from anywhere

Operations on all lists

Here are some of the methods that apply to all lists, whether resizable or not, whether immutable or not, shown by example:

var a = List.of(100, 200, 200, 500, 300, 200, 100);

a.isEmpty()                       // false
a.size()                          // 7
a.get(3)                          // 500
a.contains(300)                   // true
a.containsAll(List.of(200, 100))  // true
a.indexOf(200)                    // 1
a.lastIndexOf(200)                // 5
a.subList(2, 4)                   // [200, 500]
a.equals(List.of(100, 200, 500))  // false

You can also operate on arbitrary lists with these static methods from the Collections class:

var a = List.of(100, 200, 200, 500, 300, 200, 100);
var b = List.of(100, 200, 300, 400, 700);

Collections.disjoint(a, b)        // false
Collections.max(a)                // 500
Collections.min(a)                // 100
Collections.frequency(a, 200)     // 3
Collections.frequency(a, 500)     // 1
Collections.binarySearch(b, 400)  // 3
Collections.binarySearch(b, 100)  // 0
Collections.binarySearch(b, 500)  // -5

Operations on mutable lists

The three list methods (set, replaceAll, and sort) work on any list that allow individual elements to be changed. They don’t change the size of the list:

jshell> var a = Arrays.asList(34, 5, 2, 8)
[34, 5, 2, 8]

jshell> a.set(1, 89)
5

jshell> a
[34, 89, 2, 8]

jshell> a.replaceAll(x -> x + 10)

jshell> a
[44, 99, 12, 18]

jshell> a.sort(Comparator.naturalOrder())

jshell> a
[12, 18, 44, 99]

jshell> a.sort(Comparator.reverseOrder())

jshell> a
[99, 44, 18, 12]

You can also operate on mutable lists with these static methods from the Collections class:

jshell> var a = Arrays.asList(100, 90, 80, 70, 60, 50, 40, 30, 20, 10)
[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]

jshell> Collections.shuffle(a) ; a
[10, 30, 70, 100, 40, 20, 90, 50, 60, 80]

jshell> Collections.reverse(a) ; a
[80, 60, 50, 90, 20, 40, 100, 70, 30, 10]

jshell> Collections.sort(a) ; a
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

jshell> Collections.rotate(a, 3) ; a
[80, 90, 100, 10, 20, 30, 40, 50, 60, 70]

jshell> Collections.swap(a, 2, 7) ; a
[80, 90, 50, 10, 20, 30, 40, 100, 60, 70]

jshell> Collections.fill(a, 3) ; a
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3]

Operations on variable-sized lists

Here are some operations that actually grow or shrink a list object:

OperationDescription
a.add(e) add e to end of a
a.add(e, p) add e to a at position p
a.addAll(c) add all elements of collection c to end of a
a.addAll(c, p) add all elements of collection to a at position p
a.clear() removes all the elements from a
a.remove(o) remove object o from a
a.remove(p) remove the object at position p from a
a.retainAll(c) remove from a all elements except those in c
a.removeIf(p) remove from a all elements x for which p(x) is true

Let’s try a couple:

jshell> var a = new ArrayList<String>(List.of("Once", "I", "read", "a", "novel"))
[Once, I, read, a, novel]

jshell> a.removeIf(w -> w.length() < 3)
true

jshell> a
[Once, read, novel]
CLASSWORK
In JShell, practice with each of the above operations until you know them well.

ArrayLists vs. LinkedLists

You’ve noticed Java comes with both ArrayLists and LinkedLists. Why two kinds of lists? It turns out they have different performance characteristics. Roughly, ArrayLists are better when you do a lot of looking up values by position, and LinkedLists are better when you do a lot of inserting and deleting. Each type has some extra methods that are not common to all lists. Details can easily be found elsewhere.

The type of list elements

Okay here’s a real facepalm: Lists are not allowed to hold primitive values!

jshell> var a = new ArrayList<int>()
|  Error:
|  unexpected type
|    required: reference
|    found:    int

jshell> var a = new ArrayList<Integer>()
a ==> []

So you cannot make lists that hold boolean, byte, char, short, int, long, float, or double. But you know what? Java has built-in classes called Boolean, Byte, Character, Short, Integer, Long, Float, and Double. For the most part, instances of these classes interoperate with the values of the primitive types. It’s a little annoying, but you will be able to get used to it. Linguistically speaking the object types are called wrapper classes, and when Java automatically generates a wrapper for a primitive, it is called auto-boxing, and when Java automatically gets the primitive out of a wrapper, it is called auto-unboxing. Not all boxing and unboxing can happen automatically, though.

The type List

Because ArrayLists and LinkedLists are different kinds of lists, you’ve probably already guessed that Java has a interface type for all lists, and indeed it does. List is an interface type, and the classes ArrayList and LinkedList just happen to implement this interface, so they have all the methods specified by List.

We can take advantage of this and write operations that operate on any kind of list! Here’s a small example. We have a method can accepts any kind of Integer list (Array, Linked, or whatever List.of produces) and it returns the product of its elements:

jshell> int productOf(List<Integer> a) {
   ...>     return a.stream().reduce(1, (x, y) -> x * y);
   ...> }
|  created method productOf(List<Integer>)

jshell> productOf(List.of(3, 8, -2))
-48

jshell> var b = new ArrayList<Integer>(List.of(3, 2, 10))
[3, 2, 10]

jshell> productOf(b)
60

jshell> var b = new LinkedList<Integer>(Collections.nCopies(5, 2))
[2, 2, 2, 2, 2]

jshell> productOf(b)
32

There is so much more to interfaces, all way beyond the scope of these notes. But hopefully you appreciate the separation here between behaviors (interfaces) and the fact that different implementations of interfaces can exist.

Exercise: Now is a good time to review, or research, the differences between ArrayLists and LinkedLists.
What about methods that work on lists of elements of any type?

Oh dear, well yes, such things are possible, but there are so many aspects to this simple-sounding question, leading to wildcards, bounded wild cards, covariance vs. contravariance vs. invariance, just for starters. The page you are reading covers only the basics of Java, so you’ll have to look elsewhere for all that good stuff.

Lists and streams

If you want to use some of the cool stream operations (map, filter, flatMap, takeWhile, distinct, count, reduce, collect, min, max, findFirst, findAny, etc.) you just need to make a stream from your list.

Fortunately, it’s pretty easy: for any list a (doesn’t matter what kind of list!), you just need to write a.stream():

jshell> var a = List.of(8, 2, 9, 1, 10)

jshell> a.stream().takeWhile(x -> x % 2 == 0).count()
2

Maps (Dictionaries)

You’re probably familiar with the way a dictionary maps words to their definitions. That’s why Java calls dictionaries maps. What else can we map? How about:

  • Phrases in one language to their translations in another
  • Geographic regions to their capital
  • Words to the number of times they appear in a document
  • Words to the (list of) locations in which they appear in a document
  • Server names to IP addresses
  • Error codes to descriptions
  • Zip codes to the latitude and longitude of their post office

Here is a mapping of the regions of Eswatini to their capitals:

var eswatiniCapitals = Map.of(
    "Hhohho", "Mbabane",
    "Manzini", "Manzini",
    "Lubombo", "Siteki",
    "Shiselweni", "Nhlangano"
)

Maps map keys to values. The keys of the above map are {"Hhohho", "Manzini", "Lubombo", "Shiselweni"}. The values are {"Mbabane", "Manzini", "Siteki", "Nhlangano"}. The entries of the map is its set of “key-value pairs”. The key-value pairs are called entries.

In a map, the keys must all be unique, but the values do not have to be.

There are two main types of maps:

  • HashMap: Keys can be basically anything. Order of iteration is not defined (except there is a LinkedHashMap class which iterates in the order you inserted them).
  • TreeMap: Keys must be comparable, and iteration is done over keys in sorted order.

Learning By Doing

// Create fixed-size, read-only (immutable) maps
Map.of("Spades", "♤", "Hearts", "♡", "Diamonds", "♢", "Clubs", "♧")
Map.of()

// Create editable maps that can grow and shrink
var m = new HashMap<String, Integer>();
var m2 = new TreeMap<String, Integer>();

// Adding and/or updating
m.put("Alice", 83);          // If Alice is not already a key, add [Alice, 83]
                             // ...but if Alice is there, then update value to 83
m.putIfAbsent("Bob", 29);    // If Bob is not already a key, add [Bob, 29]
                             // ...but if Bob is there, do nothing
m2.put("Carolyne", 70);
m2.put("Bob", 55);
m.putAll(m2)                 // m is now {Carloyne=70, Bob=55, Alice=83}
m.merge("Alice", 1, Integer::sum)
                             // m is now {Carloyne=70, Bob=55, Alice=84}

// Removing
m.remove("Carolyne");        // m is now {Bob=55, Alice=83}

// Lookup by key
m.get("Alice")               // Get corresponding value for Alice if Alice is present,
                             // ...otherwise return null
m.getOrDefault("Bob", 0)     // Get corresponding value for Alice if Alice is present,
                             // ...otherwise return 0

// Iterating
for (var e : m.entrySet()) {
    // Do something with m.getKey() and m.getValue()
}

// Printing
System.out.println(m);       // Prints {Bob=55, Alice=83}

Operations on Maps

Creating maps:

OperationDescription
Map.of(k1,v1,...)a new immutable map from alternating keys and values
Map.ofEntries(e1,e2,...)a new immutable map from entries
Map.copyOf(m)a new immutable map from copying the entries of existing map m
new HashMap<K,V>()a new mutable hashmap
new TreeMap<K,V>()a new mutable treemap

If you use Map.of, Map.copyOf, or Map.ofEntries to create a map, you won’t know what kind of map you got, so do not expect any iteration order.

These operations apply to all maps:

Operation Description
m.size() the number of entries
m.isEmpty() whether there are 0 entries or not
m.get(k) value associated with key k, or null if k is not a key of m
m.getOrDefault(k,d) value associated with key k, or d is k is not a key of m
m.keySet() the set of all keys
m.values() the set of all values
m.entrySet() the set of all entries (instances of the class Map.Entry)
m.containsKey(k) whether or not k is a key of m
m.containsValue(v) whether or not v is one of the values in m
m.forEach(action) perform action(k,v) for each key-value pair (k,v) in m

and if the map is mutable, you can do these:

OperationDescription
m.put(k,v) make key k be mapped to v. Adds new entry if k was not there, otherwise replaces existing value
m.putIfAbsent(k,v)add new entry ONLY if k was not there, otherwise do nothing
m.replace(k,v)replaces the entry with key k to have value v but only if key k was already present
m.replaceAll(f)Replace each entry (k,v) with (k, f(k,v))
m.remove(k) removes entry with key k if it’s there
m.remove(k,v) removes entry with key k only if it’s mapped to v
m.clear() remove all entries of m

Map.of and Map.copyOf make immutable maps. To get mutable maps we use the constructors for HashMap or TreeMap (and you can make your own maps, too, since, you guessed it, Map is an interface!). Let’s do a tiny bit of practice with JShell, first with an immutable map:

jshell> var capitals = Map.of(
   ...>     "Hhohho", "Mbabane",
   ...>     "Manzini", "Manzini",
   ...>     "Lubombo", "Siteki",
   ...>     "Shiselweni", "Nhlangano"
   ...> )

jshell> capitals.isEmpty()
false

jshell> capitals.size()
4

jshell> capitals.keySet()
[Manzini, Hhohho, Shiselweni, Lubombo]

jshell> for (var e : capitals.entrySet()) {
   ...>     System.out.println("Capital of " + e.getKey() + " is " + e.getValue());
   ...> }
Capital of Manzini is Manzini
Capital of Hhohho is Mbabane
Capital of Shiselweni is Nhlangano
Capital of Lubombo is Siteki

jshell> capitals.forEach((k,v) -> System.out.println("Capital of " + k + " is " + v))
Capital of Manzini is Manzini
Capital of Hhohho is Mbabane
Capital of Shiselweni is Nhlangano
Capital of Lubombo is Siteki

And more examples, with a mutable map:

jshell> var counts = new HashMap<String, Integer>()

jshell> counts.put("dog", 3) ; counts
{dog=3}

jshell> counts.putAll(Map.of("rat", 2, "bat", 5)) ; counts
{bat=5, rat=2, dog=3}

jshell> counts.put("rat", 10) ; counts
{bat=5, rat=10, dog=3}

jshell> counts.putIfAbsent("rat", 5) ; counts
{bat=5, rat=10, dog=3}

jshell> counts.getOrDefault("dog", 0)
3

jshell> counts.getOrDefault("cow", 0)
0
CLASSWORK
Practice more! Create your own maps and try out each of the operations above.

Word Counting

Hey, how about an example? Let’s do the famous word count application, a popular job interview question. Given a string of comma-separated words, compute case-insensitive word frequencies, and produce, as output, a little table of the words with their count, sorted by word. Since we want our output sorted, we will gather up our entries into a TreeMap, which guarantees the entries are sorted by key when read.

For fun, we’ll show how to do this two ways. This first is very direct, straightforward, step-by-step, to the point. The second uses those fancy streams. It’s probably best to learn the non-stream way first, but you should definitely eventually learn how to use streams.

WordCountDemo.java
import java.util.TreeMap;
import java.util.stream.Stream;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.counting;

public class WordCountDemo {

    public static void printWordCountsWithoutUsingStreams(String text) {
        var counts = new TreeMap<String, Integer>();
        for (var word: text.split(",")) {
            word = word.toLowerCase().trim();
            counts.put(word, counts.getOrDefault(word, 0) + 1);
        }
        for (var e : counts.entrySet()) {
          System.out.printf("%s %d\n", e.getKey(), e.getValue());
        }
    }

    public static void printWordCountsUsingStreams(String text) {
        Stream.of(text.split(","))
            .map(String::trim)
            .collect(groupingBy(String::toLowerCase, TreeMap::new, counting()))
            .forEach((word, count) -> System.out.printf("%s %d%n", word, count));
    }

    public static void main(String[] args) {
        var text = "Dog, Q,dog, raT,DOg,q,rat,ネズミ, rat,   cat,крыса,rat,rat,";
        printWordCountsUsingStreams(text);
        System.out.println("--------");
        printWordCountsWithoutUsingStreams(text);
    }
}
Exercise: Explain the use of getOrDefault there.
This application is presented only as an illustration of what can be done with Java streams. You do not have to understand it fully right now. It is far from what you would call “basic Java” but hopefully it gets you interested and motivated to study more.

Exceptions

Things can go wrong. A good way to think about programming is that every operation can succeed or fail. If it succeeds, it should return a nice value and/or perform its expected action. If it cannot properly return a value or carry out its expected action, then it should fail, and fail hard: it should throw an exception.

In these notes so far, we’ve seen a bunch of failures, among them:

  • ArithmeticException for overflow
  • ArithmeticException for division by zero
  • ArrayIndexOutOfBoundsException when trying to access an array by an index it does not have
  • UnsupportedOperationException when trying to add to an immutable list
  • NullPointerException when trying to access a field or call a method on null.

And there are more just like them. (See here and here).

We’ve seen example already on this page where exceptions were thrown and where they were caught. As usual, there’s more to all this, but hopefully you have the basic idea.

When you write your own methods, you should be on the lookout for problems that may occur if your users pass you bad arguments. Here are some nice built-in exceptions that you can throw:

Class Used when
IllegalArgumentException Arguments passed to a method don’t make sense. This is one of the most common exceptions, if not the most. Many methods will begin with an argument check! It’s good practice if the method isn’t designed to handle all elements of a type.
IllegalStateException An object isn’t in the right state to execute this method. Maybe the object is empty, or full, or not yet initialized, or maybe it can only be modified on Tuesdays.
NoSuchElementException You’re trying to remove an element from an empty collection, or looking for a specific item that isn’t there.
UnsupportedOperationException The requested operation isn’t implemented. This is great to use when you are getting started writing a class, and haven’t written all your methods yet.

You may have the opportunity to write your own exception classes; doing so is outside the scope of these notes.

What’s Next

We’ve only toured the basics of the language. There’s so much more to learn. Here are few additional pages of notes:

  • Java Interfaces
  • Java Generics
  • Java Collections
  • Java Lambdas
  • Java Streams
  • Java Unit Testing
  • Writing Large Java Programs
  • Java Graphics
  • Java Networking
  • Java Web Servers
I know.

Most of these pages are under construction.

Finally, don’t forget the official Java development downloads, documentation, and other resources.

Summary

We’ve covered:

  • The 8 primitive types and 5 type formers
  • How to define and use variables
  • How to print
  • Some details of numbers and booleans
  • A great deal about strings
  • The fact that string indexing and length computation is nontrivial
  • A little bit about characters
  • The basics of objects and classes
  • A bit about enums
  • Introduction to interfaces
  • All the Java statements
  • Arrays, lists, and maps
  • A bit about exceptions