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.
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 Type | Description | Example Values of the Type |
---|---|---|
boolean | The two values true and false | false |
char | Code units in the range 0..65535 that are used to encode text | 'π' |
byte | Integers in the range -128 ... 127 | (byte)89 |
short | Integers in the range -32768 ... 32767 | (short)89 |
int | Integers in the range -2147483648 ... 2147483647 | 89 |
long | Integers in the range -9223372036854775808 ... 9223372036854775807 | 89L |
float | Lower precision floating point numbers | 3.95f |
double | Higher precision floating point numbers | 3.95 |
Type Former | Description | Example Type |
class | Classes |
|
[] | Arrays (a special kind of class) | boolean[] |
enum | Enums (a special kind of class) |
|
record | Record (just a shorthand way for making certain classes) |
|
interface | Interfaces |
|
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.
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
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
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
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:
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 double
sint
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:
Java | Python | JavaScript | Swift |
---|---|---|---|
int | — | — | Int32 |
BigInteger | int | BigInt | — |
double | Float | Number | Double |
BigInteger and BigDecimal are differentThe types
BigInteger
andBigDecimal
do not use+
,-
,*
and friends. Details on these types are outside the scope of these notes. We’re only doingint
anddouble
today!
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 int
s and long
s.
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.
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.
Ifx
andy
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’sBigDecimal
type (outside the scope of this tutorial) gives the programmer such control.
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
(double)(-299/100)
. This does not produce -2.99. What does it produce and why?
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).
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
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
The great thing about &&
and ||
is that they short-circuit: they don’t evaluate their right operand unless necessary. In particular:
x && y
x
is false, the whole expression is false, you’re done! Otherwise you will have to execute y
.x || y
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
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 becauseBecause the conditional operator is the only Java operator with three operands, some people like to make a big deal of this.
- unary operators have one operand,
- binary operators have two operands, and
- ternary operators have three operands.
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!
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).
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"
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.
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
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.
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}$).
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:
But Java says the length is 12 and not 7? Why? Answer: Java char
s 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 thelength
of a string, it counts code units. Here is the actual, real life break down of the example string above:
Grapheme Character Code Point Code Units Thumbs Up THUMBS UP SIGN 128077 55357, 56397 Woman Scientist WOMAN 128105 55357, 56425 ZERO WIDTH JOINER 8205 8205 MICROSCOPE 128300 55357, 56620 Dollar Sign DOLLAR SIGN 36 36 Uruguayan Flag REGIONAL INDICATOR SYMBOL LETTER U 127482 55356, 56826 REGIONAL INDICATOR SYMBOL LETTER Y 127486 55356, 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.
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:
translate
and setSize
are mutators; they changed the rectangle itself.union
is not a mutator. It returned a new rectangle without changing any of the fields of the existing rectangle.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?
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:
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.
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.
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:
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:
=
) copies the pointers (NOT the fields).==
compares the pointers (NOT the fields).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
, andnotifyAll
. These methods have default behavior that you might want to specialize for your own class. For example, the defaultequals
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!
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:
equals
, hashCode
, and toString
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 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:
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.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:
new Point
have their own x
and y
fields; the static field ORIGIN
belongs to the class and is accessed like this: Point.ORIGIN
.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.equals
, hashCode
, and toString
methods already have default implementations for all classes, we mark them with the @Override
annotation when defining our customized behaviors.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.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.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 hashCodeIt’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.
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.
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, whereasvar supervisor = null;means you don’t have a supervisor. The problem is that the expression (for example):supervisor.performPerformanceReview(employee)can throw aNullPointerException
. 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.
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:
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("."); } }
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
.)
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.
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:
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
MERCURY
through NEPTUNE
.mass
and radius
), used, for example, as follows: MARS.mass
and SATURN.radius
.surfaceGravity
and surfaceWeight
), used, for example, as follows: EARTH.surfaceGravity()
and VENUS.surfaceWeight(7.95)
.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!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();
Expressions are evaluated to produce results. Statements are executed to perform actions. In Java, statements always appear inside the body of a method.
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):
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 } }
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.
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"); }
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 switchJava 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 abreak
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.
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(); };
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();
Java has a few statements for doing something repeatedly.
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); }
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.
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()); } } }
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!)
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!"); } } }
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:
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!
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.
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.
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 Statement | Statement Type | When to Use |
---|---|---|
Simple | Empty | You want to do nothing |
Local Variable Declaration | To bring a new variable into existence | |
Assignment | To update the value in a variable | |
Increment or Decrement | To update the value in a variable by adding or subtracting 1 | |
Method Invocation | To invoke a method just for its side effects | |
Conditional | If | To do (zero or) one of a variety of different things, based on completely arbitrary conditions. |
Switch | To do (zero or) one of a variety of different things, based on the value of a single expression | |
Iteration | For | To iterate through a fixed range or collection |
While | To iterate while some condition holds (the condition is checked before each iteration) | |
Do-while | To iterate while some condition holds (the condition is checked after each iteration) | |
Disruption | Break | To immediately terminate an entire loop |
Continue | To immediately terminate the current iteration of a loop | |
Return | To immediately return from a method | |
Throw | To immediately exit the current try-block or method with an error | |
Other | Block | To group a bunch of statements together so local variable declarations can have smaller scope. |
Labeled | To give a name to a statement, either as documentation or to serve as a target of a break or continue . | |
Synchronized | To ensure some code can be executed only by one thread at a time | |
Try | To define a small section of code for error-handling or resource management | |
Local Class Declaration | To make a class used within the current block only | |
Instance Creation | To 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) |
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}
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]]
b
after the statements var a = new int[]{1, 2, 3}; var b = a; a[1] = 100;
? Why?
java.util.Arrays
class.
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 nota.equals(b)
?
I dont know. That doesn’t work for arrays. UseArrays.equals(a, b)
orArrays.deepEquals(a, b)
instead. I guess arrays are special.
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"
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.
binarySearch
and experiment with it in JShell.
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 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. 💰
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).
public static void main(String... args)
in an application.
Java has another kind of sequence, called the list. How do lists differ from arrays?
Java Arrays | Java Lists |
---|---|
Always have fixed size | You can have fixed size or variable size lists |
Always mutable | You can have mutable or immutable lists |
Elements are always packed together in memory | Can be implemented with elements tightly packed (for efficient access by index) or with links (for efficient insertion and deletion) |
Can contain primitives or objects | Can 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) instead | a.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 programming | The 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:
Code | Grow / 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 |
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
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]
Here are some operations that actually grow or shrink a list object:
Operation | Description |
---|---|
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]
In JShell, practice with each of the above operations until you know them well.
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.
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.
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.
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.
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
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:
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:
// 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}
Creating maps:
Operation | Description |
---|---|
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:
Operation | Description |
---|---|
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
Practice more! Create your own maps and try out each of the operations above.
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.
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); } }
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.
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 overflowArithmeticException
for division by zeroArrayIndexOutOfBoundsException
when trying to access an array by an index it does not haveUnsupportedOperationException
when trying to add to an immutable listNullPointerException
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.
We’ve only toured the basics of the language. There’s so much more to learn. Here are few additional pages of notes:
I know.
Most of these pages are under construction.
Finally, don’t forget the official Java development downloads, documentation, and other resources.
We’ve covered: