Learn Java

Lesson 56Phase 6: Functional and modern Java

Modern Java syntax: var, text blocks and more

The newer language features that make everyday code shorter: var, text blocks, switch expressions, compact main methods and helpful error messages.

By Updated 40 min

  • var
  • text block
  • switch expression
  • pattern matching
  • compact source file
  • String methods

A new version of Java comes out every six months, and many of them add something small that makes everyday code shorter or easier to read. This lesson collects those additions in one place. Some are new to you: var, text blocks for text that runs over several lines, a few newer String methods, and the underscore _ for values you never use. Others you have met already, and here they work together in one program. A table at the end says which Java version brought each feature, so you know what you can use on an older Java at school or at work.

By the end of this lesson
  • Declare local variables with var, say where Java refuses it, and decide when writing the type out reads better.
  • Write text that spans several lines as a text block, and predict the exact string from its indentation and its closing delimiter.
  • Keep spaces at the end of a line with \s, join lines with a backslash, and fill in values with formatted.
  • Use the newer String methods: strip, lines, indent, stripIndent and chars.
  • Combine record patterns, when guards and _ in one switch, and use _ for any variable you must declare but never read.
  • Say which Java version made each feature on this page standard.
From before
Much of this page builds on earlier lessons. You met var briefly in variables and types, and isBlank, repeat and strip in strings. The diamond <> from generics comes back next to var. The methods lines and chars hand you streams, and formatted fills in the same %s and %d blanks as String.format from money and exact numbers. Switch expressions, records, sealed types and patterns each had a lesson of their own, and this page links back to them where they come up.

var: let the compiler write the type#

Here is the same variable from a bookshop program, declared three ways:

Three ways to declare stock
read along
1TreeMap<String, Integer> stock = new TreeMap<String, Integer>();
2TreeMap<String, Integer> stock = new TreeMap<>();
3var stock = new TreeMap<String, Integer>();

The first line names the type twice. The diamond from generics lets you drop the second copy. Since Java 10 you can drop the first copy instead, by writing var. It tells the compiler: work out this variable's type from the value on the right. The compiler sees new TreeMap<String, Integer>(), so stock gets exactly that type, the same as in the first line.

This is called local variable type inference. Inference means the compiler works the type out for itself. Local means it only works for local variables, the ones declared inside a method, which exist only while the method runs.

Bookshop.java
1import java.util.ArrayList;
2import java.util.TreeMap;
3
4public class Main {
5 public static void main(String[] args) {
6 var copies = 3;
7 var price = 9.99;
8 var title = "Dune";
9
10 var tags = new ArrayList<String>();
11 tags.add("sci-fi");
12 tags.add("classic");
13
14 var stock = new TreeMap<String, Integer>();
15 stock.put("Dune", copies);
16 stock.put("Emma", 5);
17
18 for (var entry : stock.entrySet()) {
19 System.out.println(entry.getKey() + ": " + entry.getValue());
20 }
21 System.out.println(title + " costs " + price + ", tags " + tags);
22 }
23}
Prints
›Dune: 3
›Emma: 5
›Dune costs 9.99, tags [sci-fi, classic]

Step through it. On the first three lines the narration says Declare int copies, Declare double price and Declare String title, and the memory view shows each variable's type next to its name. The compiler read 3 as an int, 9.99 as a double and the text in quotes as a String. The loop variable entry is where var saves the most typing: its full type is Map.Entry<String, Integer>. The program prints Dune: 3, Emma: 5 and Dune costs 9.99, tags [sci-fi, classic].

The type is still fixed

var does not make a variable flexible. Once the compiler has picked a type, the variable keeps it for life, exactly as if you had written the type yourself. Try to put text into a variable that started out as a number:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 var count = 10;
4 count = "ten";
5 System.out.println(count);
6 }
7}
Terminal
read along
1$ javac Main.java
2Main.java:4: error: incompatible types: String cannot be converted to int
3 count = "ten";
4 ^
51 error

That is the message you would get after int count = 10; as well. The static typing you met in variables and types works the same with or without var.

Coming from JavaScript or Python?
In JavaScript, a variable made with var or letcan hold a number now and a string later, and Python variables work the same way. Java's var looks alike and does much less: it only saves you typing the type. Java still gives the variable one type and checks every use of it before the program runs.
Key idea

var asks the compiler to copy the type from the starting value. The variable still has one fixed type, checked before the program runs, exactly as if you had written it out.

Where var works

You can use var wherever you declare a local variable and give it a value in the same statement:

  • A local variable with a starting value: var total = 0;
  • The counter of a for loop: for (var i = 0; i < 3; i++)
  • The variable of an enhanced for loop: for (var entry : stock.entrySet())
  • A resource in try-with-resources, from reading files: try (var br = new BufferedReader(new FileReader("notes.txt")))
  • The parameters of a lambda, since Java 11: (var a, var b) -> a + b. Either every parameter uses var or none does.

The lambda form looks pointless, since (a, b) -> a + b already leaves the types out. It exists so that you can put an annotation on a lambda parameter without writing its type, and annotations are the subject of the next lesson.

Where var does not work

var needs a value to copy a type from. These are the two mistakes people make first:

Main.java
read along
1public class Main {
2 public static void main(String[] args) {
3 var total;
4 total = 42;
5
6 var winner = null;
7 System.out.println(total + " " + winner);
8 }
9}
Terminal
read along
1$ javac Main.java
2Main.java:3: error: cannot infer type for local variable total
3 var total;
4 ^
5 (cannot use 'var' on variable without initializer)
6Main.java:6: error: cannot infer type for local variable winner
7 var winner = null;
8 ^
9 (variable initializer is 'null')
102 errors

The first error is about the missing starting value, which javac calls an initializer. int total; followed by total = 42; is fine, because the type is written down, but with var the compiler would have nothing to read the type from. The second error is about null. It means “no object”, and it says nothing about which kind of object might arrive later, so there is no type to copy.

A few more places where javac says no:

What you writeWhat javac saysWrite this instead
var scores = {90, 85, 77};array initializer needs an explicit target-typevar scores = new int[] {90, 85, 77};
var shout = s -> s.toUpperCase();lambda expression needs an explicit target-typeFunction<String, String> shout = s -> s.toUpperCase();
var on a field, a method parameter or a return type'var' is not allowed herethe type, written out
var a = 1, b = 2;'var' is not allowed in a compound declarationone variable per declaration
The first two messages start with cannot infer type for local variable and the name of the variable, then give the reason in parentheses.

The braces of an array and the arrow of a lambda only make sense once Java knows the type they are for, so var cannot work it out from them. The last two rows show the other limit: var is only for local variables. Fields, parameters and return types are how a class talks to the rest of the program, so Java wants their types written where every reader can see them.

When var makes code harder to read

Code is read far more often than it is written, and var hides the type from the reader. That costs nothing when the right-hand side shows the type anyway. It hurts in three situations.

The first is a method call whose result is not obvious. In var result = library.search("dune");, is result one book, a list of books or an Optional? The reader has to go and find search to know. Writing List<Book> result answers the question on the spot, and so does a better name such as matchingBooks.

The second is a plain number. A whole number written without a decimal point is always an int, so var makes an int even when you meant something else. Decide what this prints before you run it.

Predict the outputPizza.java
1public class Main {
2 public static void main(String[] args) {
3 var slices = 10;
4 var friends = 4;
5 System.out.println(slices / friends);
6
7 var price = 10.0;
8 System.out.println(price / friends);
9 }
10}

Each var takes its type from its starting value. What do the two lines print?

It prints 2 and then 2.5. 10 made slices an int, so slices / friends is integer division, the surprise from operators and casting. 10.0 made price a double, and the division kept its half. With the types written out, int slices = 10; would have shown the problem at a glance.

The third is the diamond. In var names = new ArrayList<>(); nothing says what the list holds, so Java picks the most general type there is, Object, and names becomes an ArrayList<Object>. The mistake shows up later, far from its cause:

Main.java
read along
1import java.util.ArrayList;
2
3public class Main {
4 public static void main(String[] args) {
5 var names = new ArrayList<>();
6 names.add("Ada");
7 names.add("Grace");
8
9 String first = names.get(0);
10 System.out.println(first.toUpperCase());
11 }
12}
Terminal
read along
1$ javac Main.java
2Main.java:9: error: incompatible types: Object cannot be converted to String
3 String first = names.get(0);
4 ^
51 error

With var, put the type arguments on the right: var names = new ArrayList<String>();. Or keep the type on the left and the diamond on the right, as you did in generics. Here is a rule of thumb drawn from the guide the Java team wrote on using var:

var reads well
Write the type out
The right side names the type: new TreeMap<String, Integer>(), text in quotes, a number with a decimal point.
The right side is a method call, and the reader would have to look up what it returns.
The type is long, and the name says what the variable holds, like entry in a loop over a map.
A whole number where you meant a long or a double.
The variable is used for a few lines and then forgotten.
The variable is used far from where it was declared, in a long method.
Let the name carry the meaning
When you switch a declaration to var, look at the variable's name. With the type gone, the name has to say what the value is: count instead of c, matchingBooks instead of list. Most of this course still writes types out, because seeing them helps while you learn.
Quick check
Which of these lines compiles?

Text blocks: text that keeps its shape#

Writing several lines of text in an ordinary string is fiddly. Every line needs a \n at the end and a + to join it to the next, and every quotation mark inside the text needs a backslash in front. Here is a short note to a friend written that way, and the same note as a text block:

Note.java
1public class Main {
2 public static void main(String[] args) {
3 String oldWay = "Dear Sam,\n"
4 + "Chapter 3 made me say \"wow\" out loud.\n"
5 + "See you Friday,\n"
6 + "Ada\n";
7
8 String newWay = """
9 Dear Sam,
10 Chapter 3 made me say "wow" out loud.
11 See you Friday,
12 Ada
13 """;
14
15 System.out.print(newWay);
16 System.out.println(oldWay.equals(newWay));
17 }
18}
Prints
›Dear Sam,
›Chapter 3 made me say "wow" out loud.
›See you Friday,
›Ada
›true

A text block starts with three double quotes, """, and a line break. The text begins on the next line and runs until the closing """. Line breaks are real line breaks, and quotation marks need no backslash. The program prints the note and then true: both versions build exactly the same string.

A text block is just another way to write a String. It works anywhere a string in quotes would, and every String method works on it. Text blocks became a standard part of Java in Java 15, and they are the usual way to put SQL, JSON or HTML into a Java program.

The opening quotes end their line
Nothing but spaces may follow the opening """ on its line, because the text always starts on the next one. String s = """Hello"""; does not compile: javac reports illegal text block open delimiter sequence, missing line terminator. For one line of text, use ordinary quotes.

Which spaces Java keeps#

In the source code, every line of that note starts with 16 spaces, which line it up with the code around it. None of those spaces reach the output: Java removes them on purpose. The designers of text blocks call them incidental white space, spaces that only come from where the code sits.

Java looks at every line that has text on it, and at the line of the closing delimiter when the delimiter stands on its own. It finds the smallest indentation among those lines and removes that many spaces from the start of every line. Any extra indentation a line has stays in the string. Then Java removes the spaces at the end of each line.

Menu.java
1public class Main {
2 public static void main(String[] args) {
3 String menu = """
4 Breakfast
5 Eggs
6 Toast
7 Lunch
8 Soup
9 """;
10 System.out.print(menu);
11 }
12}
Prints
›Breakfast
› Eggs
› Toast
›Lunch
› Soup

Breakfast, Lunch and the closing delimiter are the least indented lines, at 16 spaces, so Java removes 16 from every line. The dishes sit 4 spaces further in, and they keep those 4:

Output
read along
1Breakfast
2 Eggs
3 Toast
4Lunch
5 Soup
Key idea

Java removes the indentation that all the lines share, counting the closing delimiter's line when it stands alone, and the spaces at the end of each line. The rest of what you typed stays.

Where the closing delimiter goes#

Because the closing delimiter's line can count, where you put it changes the string:

  • On its own line, lined up with the text, as in the menu. The string ends with a line break after the last line, and no extra indentation is kept.
  • At the end of the last line of text, as in Soup""";. That line holds text, so only the text lines count, and the string ends right after Soup with no final line break.
  • On its own line, further left than the text. Now it is the least indented line, so Java removes less, and every line keeps the difference. At the very start of its line, it makes Java remove nothing at all.

Further right than the text makes no difference, because the text lines are then the least indented. Try each position in the lab. It writes the block at the left edge of the code, so the numbers stay small: Breakfast and Lunch start 4 spaces in, the dishes 8.

Text block lab

Each dot is a space in the source code. Move the closing """ and watch which spaces Java keeps.

Closing delimiter
4 spaces before it
After Toast
String menu = """
(4 removed spaces) Breakfast
(4 removed spaces) (4 kept spaces) Eggs
(4 removed spaces) (4 kept spaces) Toast
(4 removed spaces) Lunch
(4 removed spaces) (4 kept spaces) Soup
(4 removed spaces) """;
removed by Javakept in the string

The string in menu, line by line

Breakfast(line break)
(4 kept spaces) Eggs(line break)
(4 kept spaces) Toast(line break)
Lunch(line break)
(4 kept spaces) Soup(line break)

The closing delimiter lines up with Breakfast and Lunch, at 4 spaces, so Java removes 4 from every line. The dishes keep the 4 extra spaces that set them apart.

Now check yourself on the two most common positions. The square brackets in the output show where each string starts and ends.

Predict the outputColors.java
1public class Main {
2 public static void main(String[] args) {
3 String a = """
4 red
5 green""";
6 String b = """
7 red
8 green
9 """;
10 System.out.println("[" + a + "]");
11 System.out.println("[" + b + "]");
12 }
13}

The two text blocks differ only in where the closing delimiter sits. Write the lines this program prints.

It prints five lines: [red, green], [red, green and a line holding only ]. The string a ends right after green, so its closing bracket follows at once. The string b ends with a line break, so its closing bracket lands on a line of its own.

Two escapes made for text blocks#

The usual escapes such as \t still work inside a text block, and two more are useful here:

  • A backslash at the very end of a line joins that line to the next. It lets you keep a long sentence on short lines in your code without putting a line break into the string.
  • \s stands for one space. Java translates escapes last, after it has removed the spaces at the ends of lines, so a \s at the end of a line protects the spaces in front of it, and then becomes a space itself.
Escapes.java
1public class Main {
2 public static void main(String[] args) {
3 String sentence = """
4 Java removes the line break \
5 after a backslash.""";
6 System.out.println(sentence);
7
8 String plain = """
9 Total """;
10 String kept = """
11 Total \s""";
12 System.out.println("[" + plain + "]");
13 System.out.println("[" + kept + "]");
14 }
15}
Prints
›Java removes the line break after a backslash.
›[Total]
›[Total ]
Output
read along
1Java removes the line break after a backslash.
2[Total]
3[Total ]

The first text block prints as one line. In plain, the three spaces after Total are removed, even though you can see them in front of the closing delimiter. In kept, the two spaces in front of \s survive and \s adds a third, so three spaces sit before the closing bracket. Spaces at the end of a line matter in a few places, such as files where every line must be exactly the same length.

Three quotation marks inside a text block
One or two " in a row need no escape. Three in a row would end the block, so write \""" to put three quotation marks into the text.

Filling in values with formatted#

A text block has no way to drop a variable straight into the text. Joining pieces with + works, but it chops up the shape you were trying to keep. The tidy way is the method formatted, added in Java 15 for this job. It works like String.format, called on the text itself: each blank, such as %s or %d, is filled in from the values you pass, in order.

Booking.java
1public class Main {
2 public static void main(String[] args) {
3 String guest = "Mia";
4 int nights = 3;
5 double price = 89.5;
6
7 String confirmation = """
8 Hello %s,
9 Your room is booked for %d nights.
10 Total to pay: %.2f
11 """.formatted(guest, nights, nights * price);
12
13 System.out.print(confirmation);
14 }
15}
Prints
›Hello Mia,
›Your room is booked for 3 nights.
›Total to pay: 268.50

%s takes guest, %d takes the whole number of nights, and %.2f takes the total and shows it with two decimal places, so the last line reads Total to pay: 268.50. "...".formatted(a, b) gives the same result as String.format("...", a, b). On a text block, formatted keeps the values right next to the text they fill.

Put the lines in orderGameOver.java
Put the lines in order so the program prints Game over, Ravi! and then Final score: 1200. The values must exist before the text block can use them.

Tap a line, then tap where it should go. Or use the arrows.

ExerciseSlip.java
A library prints a slip when you borrow a book. Build slip with a text block and formatted so the program prints four lines: Borrowed: Dune, then by Frank Herbert and Due back in 21 days, each indented by two spaces, then Enjoy the book, Ravi!. The slip ends with a line break.

Newer String methods#

Java 11 and the versions after it gave String a set of methods for jobs that used to need a loop. Like every String method, each one hands back a new value and leaves the original alone. You met three of them in strings: isBlank, repeat and strip. This section shows what strip does that trim does not, and adds the rest.

strip, stripLeading and stripTrailing

strip() removes white space from both ends, like trim(). Its two halves clean one end only: stripLeading() the start and stripTrailing() the end. The difference from trim() is what counts as white space.

Strip.java
1public class Main {
2 public static void main(String[] args) {
3 String typed = " Ada Lovelace ";
4 System.out.println("[" + typed.strip() + "]");
5 System.out.println("[" + typed.stripLeading() + "]");
6 System.out.println("[" + typed.stripTrailing() + "]");
7
8 String wide = "\u3000Ada\u3000";
9 System.out.println(wide.trim().length());
10 System.out.println(wide.strip().length());
11
12 String spaces = " ";
13 System.out.println(spaces.isEmpty());
14 System.out.println(spaces.isBlank());
15 }
16}
Prints
›[Ada Lovelace]
›[Ada Lovelace ]
›[ Ada Lovelace]
›5
›3
›false
›true
Output
read along
1[Ada Lovelace]
2[Ada Lovelace ]
3[ Ada Lovelace]
45
53
6false
7true

The brackets show what each of the first three calls kept. The next two lines show the difference from trim(). \u3000 is the wide space used in Chinese and Japanese text. trim() only knows the ordinary space and the invisible control characters below it, so it leaves both wide spaces and the length stays 5. strip() removes every character Java counts as white space, so only the three letters of Ada remain: 3. The last two lines are the recap from strings: three spaces are not empty, so isEmpty() gives false, but they are blank, so isBlank() gives true.

Reach for strip and isBlank
In new code, use strip() where older code uses trim(), and check typed input with isBlank(): a name made of three spaces is not a name. One character still slips through: the non-breaking space that web pages use, \u00A0, is not white space to Java, so neither strip() nor trim() removes it.

repeat and lines

repeat(n) gives the string n times in a row, which is handy for dividing lines and bars in console output. lines() cuts a string at its line breaks and hands you the lines as a Stream<String>, without the line breaks, so every stream method from streams is ready to use.

Song.java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("=".repeat(24));
4 System.out.println("Ha".repeat(3));
5
6 String song = """
7 Row, row, row your boat
8 Gently down the stream
9
10 Merrily, merrily
11 """;
12 System.out.println(song.lines().count());
13
14 song.lines()
15 .filter(line -> !line.isBlank())
16 .map(String::toUpperCase)
17 .forEach(System.out::println);
18 }
19}
Prints
›========================
›HaHaHa
›4
›ROW, ROW, ROW YOUR BOAT
›GENTLY DOWN THE STREAM
›MERRILY, MERRILY

It prints a row of 24 equals signs, then HaHaHa, then 4. The song has 4 lines: the empty line in the middle counts, and the line break at the very end only finishes the last line, so it does not start a fifth. The stream then drops the blank line and prints the other three in capitals.

indent and stripIndent

indent(n) moves every line n spaces to the right, or up to n spaces to the left when n is negative, and makes sure the result ends with a line break. stripIndent() runs the text block rule on any string: it removes the indentation that all the lines share, and the spaces at the ends of lines. That helps with text your program builds or reads, such as a quote copied out of an indented e-mail.

Indent.java
1public class Main {
2 public static void main(String[] args) {
3 String recipe = "Mix the flour\nAdd two eggs";
4 System.out.print(recipe.indent(4));
5 System.out.print(" Bake for 20 minutes\n".indent(-6));
6
7 String quote = " To be, or not to be,\n that is the question.";
8 System.out.println(quote.stripIndent());
9 }
10}
Prints
› Mix the flour
› Add two eggs
› Bake for 20 minutes
›To be, or not to be,
›that is the question.
Output
read along
1 Mix the flour
2 Add two eggs
3 Bake for 20 minutes
4To be, or not to be,
5that is the question.

indent(4) pushes both recipe lines 4 spaces to the right and adds a line break after the second, which is why print is enough. indent(-6) takes 6 of the 8 spaces in front of Bake away. stripIndent() sees that both lines of the quote start with 8 spaces and removes all 8.

chars: the characters as numbers

chars() is older than the rest, from Java 8, and it surprises nearly everyone once. It returns an IntStream, a stream of int values, with one number for each character. Decide what this prints.

Predict the outputHi.java
1public class Main {
2 public static void main(String[] args) {
3 "Hi!".chars().forEach(c -> System.out.println(c));
4 }
5}

The stream holds one value for each character of Hi!. Pick the output.

It prints 72, 105 and 33. A char is stored as a number, its code in the character table, as you saw in operators and casting, and chars() hands you those numbers. To turn one back into a character, cast it: (char) c. Often you do not need to, because comparing with a character such as c == 's' works on the numbers directly, and so do Character methods such as isDigit:

Chars.java
1public class Main {
2 public static void main(String[] args) {
3 String word = "Mississippi";
4 long esses = word.chars().filter(c -> c == 's').count();
5 System.out.println(esses);
6
7 String pin = "4071";
8 System.out.println(pin.chars().allMatch(Character::isDigit));
9 System.out.println("40x1".chars().allMatch(Character::isDigit));
10 }
11}
Prints
›4
›true
›false

It prints 4, true and false. Counting one letter takes a single line, and so does checking that a PIN is made only of digits.

MethodWhat it gives backSince
strip(), stripLeading(), stripTrailing()a new string without white space at both ends, at the start, or at the endJava 11
isBlank()true when the string is empty or only white spaceJava 11
repeat(n)the string n times in a rowJava 11
lines()a Stream<String> of the lines, without line breaksJava 11
indent(n)every line moved n spaces, ending with a line breakJava 12
stripIndent()the text block indentation rule, applied to any stringJava 15
formatted(...)the string with its blanks filled in, like String.formatJava 15
chars()an IntStream of character codesJava 8
Every method here returns something new and leaves the original string unchanged.
Quick check
How many lines does "tea\n\ncake\n".lines().count() count?
ExerciseShopping.java
Someone pasted a shopping list from their phone. It has stray spaces, empty lines, and a line made only of spaces. Print each real item stripped and numbered from 1, as in 1. milk, then the number of items.

Switch, records and patterns in one program#

You have met three more of Java's newer features in earlier lessons. A switch expression produces a value, with yield for a case that needs several lines, from making decisions. A record holds plain data in one line, from records. Patterns test what kind of value you have and take it apart, and a sealed type closes the list of kinds, from sealed types and pattern matching. Modern Java code uses them together, so here they are in one program: a music app's queue of songs, podcasts and adverts.

Playlist.java
1public class Main {
2 public static void main(String[] args) {
3 Media[] queue = {
4 new Song("Clair de Lune", 302),
5 new Song("Intro", 45),
6 new Podcast("Java Weekly", 12, 74),
7 new Ad("Sunny Juice", 15)
8 };
9 for (Media item : queue) {
10 System.out.println(describe(item));
11 }
12
13 if (queue[2] instanceof Podcast(var show, _, var minutes) && minutes > 60) {
14 System.out.println("Save " + show + " for the train");
15 }
16 }
17
18 static String describe(Media item) {
19 return switch (item) {
20 case Song(var title, var seconds) when seconds < 60 -> "A short song: " + title;
21 case Song(var title, var seconds) -> title + ", " + seconds / 60 + " min";
22 case Podcast(var show, var episode, var minutes) -> {
23 String size = minutes > 60 ? "long" : "short";
24 yield show + " #" + episode + ", a " + size + " one";
25 }
26 case Ad _ -> "An advert";
27 };
28 }
29}
30
31sealed interface Media permits Song, Podcast, Ad {}
32record Song(String title, int seconds) implements Media {}
33record Podcast(String show, int episode, int minutes) implements Media {}
34record Ad(String brand, int seconds) implements Media {}
Prints
›Clair de Lune, 5 min
›A short song: Intro
›Java Weekly #12, a long one
›An advert
›Save Java Weekly for the train

The sealed interface Media says there are exactly three kinds of item, each a record. Read describe from the top:

  • case Song(var title, var seconds) when seconds < 60 is a record pattern with a guard. It matches a song, unpacks its two components into variables, and takes only songs shorter than a minute.
  • The next case takes every other song. It has to come after the guarded one, or it would take all songs and javac would reject the guarded case as dominated.
  • The podcast case needs two lines, so its arrow leads to a block that ends with yield.
  • case Ad _ is a type pattern without a name, because describing an advert needs nothing from inside it.
  • There is no default. Media is sealed, so these cases cover every possible item, and javac checks that when it compiles the program.

The if at the end of main uses a record pattern with instanceof. Podcast(var show, _, var minutes) unpacks the show and the length and skips the episode number, and the test after && can use minutes because it only runs when the pattern matched. The program prints Clair de Lune, 5 min, A short song: Intro, Java Weekly #12, a long one, An advert and Save Java Weekly for the train.

The unnamed variable _#

You just saw _ twice: for the episode number nobody needed, and in place of a name for the advert nobody looked inside. Since Java 22, a single underscore can stand in for a variable that Java makes you declare but that you never read. It is called the unnamed variable, and it tells the reader that the value is unused on purpose. It works outside patterns too:

Unused.java
1import java.util.TreeMap;
2
3public class Main {
4 public static void main(String[] args) {
5 var plays = new TreeMap<String, Integer>();
6 plays.put("Blue", 12);
7 plays.put("Intro", 3);
8 plays.forEach((title, _) -> System.out.println(title));
9
10 try {
11 int volume = Integer.parseInt("loud");
12 System.out.println(volume);
13 } catch (NumberFormatException _) {
14 System.out.println("Volume must be a number");
15 }
16 }
17}
Prints
›Blue
›Intro
›Volume must be a number

It prints Blue, Intro and Volume must be a number. forEach on a map hands its lambda two values, a key and a value. This one only prints titles, so the play count is _. A catch must declare its exception even when the block never uses it, so that is _ as well. The same goes for the variable of an enhanced for loop that only counts its turns. Three rules come with it:

  • You cannot read _. System.out.println(_); stops javac with underscore not allowed here.
  • One method can declare several, even side by side as in (_, _) -> 0, because none of them has a name to clash with.
  • It is for local variables, lambda and catch parameters, and patterns. A field or a method parameter cannot be _.
Key idea

Write _ where Java makes you name something you will never read: a part of a pattern, a caught exception, a lambda parameter. The reader then knows it is unused on purpose.

Quick check
Each line uses _. Which one does not compile?

Compact source files and helpful error messages#

Compact source files

Your very first program, in your first Java program, was a compact source file: a void main() with no class written around it, printing with IO.println. Java 25 made that form standard. Two more facts about it are useful now. A compact source file may declare records, classes and other methods next to main. And it can use every public class in Java's core library, the part called java.base that holds java.util, java.io and java.time, without a single import line. Here is a small program in that form that uses several features from this page:

Receipt.java
1record Item(String name, int cents) {}
2
3void main() {
4 var basket = List.of(new Item("Bread", 145), new Item("Milk", 89), new Item("Jam", 230));
5 var total = 0;
6 for (var item : basket) {
7 total += item.cents();
8 }
9 var receipt = """
10 Items: %d
11 Total: %.2f
12 """.formatted(basket.size(), total / 100.0);
13 IO.print(receipt);
14}
Prints
›Items: 3
›Total: 4.64

It prints Items: 3 and Total: 4.64. List needed no import, and the record Item sits right in the file. The classic form you see everywhere else in this course still matters: real projects are made of many classes in many files, and compact files are meant for small programs like this one.

Helpful NullPointerException messages

In reading errors and debugging you read a message such as Cannot invoke "String.length()" because "last" is null. Java 14 added these detailed messages, and since Java 15 they are switched on by default. They help most in a chain of calls, where more than one value could be the null:

Chain.java
1public class Main {
2 public static void main(String[] args) {
3 Customer grace = new Customer(new Address("Arlington"));
4 Customer ada = new Customer(null);
5
6 System.out.println(grace.getAddress().getCity());
7 System.out.println(ada.getAddress().getCity());
8 }
9}
10
11class Customer {
12 private Address address;
13
14 Customer(Address address) {
15 this.address = address;
16 }
17
18 Address getAddress() {
19 return address;
20 }
21}
22
23class Address {
24 private String city;
25
26 Address(String city) {
27 this.city = city;
28 }
29
30 String getCity() {
31 return city;
32 }
33}
Prints
›Arlington
!Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.getCity()" because the return value of "Customer.getAddress()" is null
! at Main.main(Main.java:7)
Stops on line 7

NullPointerException: Cannot invoke "Address.getCity()" because the return value of "Customer.getAddress()" is null

A variable held null, so there was no object to use.

Grace has an address, so the first line prints Arlington. Ada's address is null, and the second line stops the program with this message: Cannot invoke "Address.getCity()" because the return value of "Customer.getAddress()" is null. It names both halves of the problem. The call that failed is getCity(), and the value it was called on was the null that getAddress() returned. Before Java 14 the exception carried no message at all, only a line number, and you had to work out for yourself which call was to blame. If a customer may really have no address, the Optional from the previous lesson says so in the type.

Which Java version brought what#

Since 2018 a new Java has come out every March and every September. A new language feature usually spends a version or more as a preview, which you have to switch on with the --enable-preview option, before it becomes a standard part of Java. The table lists the version where each feature became standard. The versions marked LTS get long-term support, so most companies run one of them. If your job uses Java 17, everything in the rows up to Java 17 works there, and nothing below it does.

VersionReleasedWhat became standard
Java 8 (LTS)March 2014Lambdas, method references, streams and Optional
Java 10March 2018var for local variables (JEP 286)
Java 11 (LTS)September 2018var in lambda parameters (JEP 323), the String methods strip, isBlank, lines and repeat, and running a file with java Main.java (JEP 330)
Java 14March 2020Switch expressions and yield (JEP 361), and helpful NullPointerException messages (JEP 358), on by default from Java 15
Java 15September 2020Text blocks (JEP 378), with stripIndent and formatted
Java 16March 2021Records (JEP 395) and instanceof patterns (JEP 394)
Java 17 (LTS)September 2021Sealed classes and interfaces (JEP 409)
Java 21 (LTS)September 2023Patterns in switch, with when and case null (JEP 441), and record patterns (JEP 440)
Java 22March 2024The unnamed variable and pattern _ (JEP 456)
Java 25 (LTS)September 2025Compact source files, void main() and the IO class (JEP 512)
A JEP (JDK Enhancement Proposal) is the public document that describes a change to Java. The String method indent arrived in Java 12.
Quick check
Your team's servers run Java 17. Which of these can you use there?

Try it

Answer each one in your head or on paper first, then open the answer.

1
Rewrite this declaration with var, and say what type groups has afterwards: HashMap<String, List<String>> groups = new HashMap<String, List<String>>();
2
Why does var total; fail to compile, when int total; is fine?
3
In a text block, Breakfast and Lunch are indented 12 spaces and the dishes 16, and the closing delimiter sits on its own line at 8 spaces. How many spaces does each line start with in the string?
4
What does this program print? Work out each line before you open the answer.
5
A program stops with Cannot invoke "Order.getTotal()" because the return value of "Customer.lastOrder()" is null. Which method returned null, and which call failed?
6
In case Podcast(var show, _, var minutes) ->, what does the _ do, and can the code after the arrow use it?

Practice

Work through these on your own. Each one builds on this page.

1
Take a program you wrote for an earlier lesson, such as the word counter or the grade book, and change every local variable you can to var. Then put the type back wherever the right-hand side no longer tells a reader what the variable holds. Write one sentence for each type you put back.
2
Write a method String box(String text) that draws a frame around text, so that box("Hello") returns three lines: +-------+, | Hello | and +-------+. Then make it work for text with several lines, padding each line to the width of the longest.
3
Write a text block that holds a book as JSON, such as {"title": "Dune", "pages": 412} spread over four lines, with the title and the page count filled in by formatted. Print it. Then move the closing delimiter 4 spaces to the left, predict what changes in the output, and run it again to check.
4
Add a Video record with a title, a length in minutes and a boolean for subtitles to the playlist's sealed Media family. On your own computer, compile before you touch describe and write down what javac says. Then add a case for videos with subtitles, using a guard, and one for the rest, with _ for every component a case does not need.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email