Chapter 4. The Java Language

As humans, we learn the subtleties of spoken language through trial and error. We learn where to put the subject in relation to the verb and how to handle things like tenses and plurals. We certainly learn advanced language rules in school, but even the youngest students can ask their teachers intelligible questions. Computer languages have similar features: there are “parts of speech” that work as composable building blocks. There are ways of declaring facts and asking questions. In this chapter, we look at those fundamental programming units in Java. Trial and error remains a great teacher, so we’ll also look at how to play with these new units and practice your skills.

Since Java’s syntax is derived from C, we make some comparisons to features of that language, but no prior knowledge of C is necessary. Chapter 5 builds on this chapter by talking about Java’s object-oriented side and completing the discussion of the core language. Chapter 7 discusses generics and records, features that enhance the way types work in the Java language, allowing you to write certain kinds of classes more flexibly and safely.

After that, we dive into the Java APIs and see what we can do with the language. The rest of this book is filled with brief examples that do useful things in a variety of areas. If you are left with any questions after these introductory chapters, we hope they’ll be answered as you look at the code. There is always more to learn, of course! We’ll try to point out other resources along the way that might benefit folks looking to continue their Java journey beyond the topics we cover.

For readers just beginning their programming journey, the web will likely be a constant companion. Many, many sites, Wikipedia articles, blog posts, and, well, the entirety of Stack Overflow can help you dig into particular topics or answer small questions that might arise. For example, while this book covers the Java language and how to start writing useful programs with Java and its tools, we don’t cover lower, core programming topics such as algorithms in much detail. These programming fundamentals will naturally appear in our discussions and code examples, but you might enjoy a few hyperlink tangents to help cement certain ideas or fill in the gaps we must necessarily leave.

As we have mentioned before, many terms in this chapter will be unfamiliar. Don’t worry if you are occasionally a little confused. The sheer breadth of Java means we have to leave out explanations or background details from time to time. As you progress, we hope you’ll have the chance to revisit some of these early chapters. New information can work a bit like a jigsaw puzzle. It’s easier to fit a new piece if you already have some other, related pieces connected. When you’ve spent some time writing code and this book becomes more of a reference for you and less of a guide, you’ll find the topics in these first chapters make more sense.

Text Encoding

Java is a language for the internet. Since individual users speak and write in many different human languages, Java must be able to handle a large number of languages as well. It handles internationalization through the Unicode character set, a worldwide standard that supports the scripts of most languages.1 The latest version of Java bases its character and string data on the Unicode 14.0 standard, which uses at least two bytes to represent each symbol internally. As you may recall from “The Past: Java 1.0–Java 20”, Oracle endeavors to keep up with new releases of the Unicode standard. Your version of Java may include a newer version of Unicode.

Java source code can be written using Unicode and stored in any number of character encodings. This makes Java a fairly friendly language for including non-English content. Programmers can use Unicode’s rich set of characters not only for displaying information to the user but also in their own class, method, and variable names.

The Java char type and String class natively support Unicode values. Internally, the text is stored using either character or byte arrays; however, the Java language and APIs make this transparent to you, and you generally will not have to think about it. Unicode is also very ASCII friendly (ASCII is the most common character encoding for English). The first 256 characters are defined to be identical to the first 256 characters in the ISO 8859-1 (Latin-1) character set, so Unicode is effectively backward compatible with the most common English character sets. Furthermore, one of the most common file encodings for Unicode, called UTF-8, preserves ASCII values in their single byte form. This encoding is used by default in compiled Java class files, so storage remains compact for English text.

Most platforms can’t display all currently defined Unicode characters. As a workaround, Java programs can be written with special Unicode escape sequences. A Unicode character can be represented with this escape sequence:


xxxx is a sequence of one to four hexadecimal digits. The escape sequence indicates an ASCII-encoded Unicode character. This is also the form Java uses to output (print) Unicode characters in an environment that doesn’t otherwise support them. Java comes with classes to read and write Unicode character streams in specific encodings, including UTF-8.

As with many long-lived standards in the tech world, Unicode was originally designed with so much extra space that no conceivable character encoding could ever possibly require more than 64K characters. Sigh. Naturally we have sailed past that limit and some UTF-32 encodings are in popular circulation. Most notably, emoji characters scattered throughout messaging apps are encoded beyond the standard range of Unicode characters. (For example, the canonical smiley emoji has the Unicode value 1F600.) Java supports multibyte UTF-16 escape sequences for such characters. Not every platform that supports Java will support emoji output, but you can fire up jshell to find out if your environment can show emoji characters (see Figure 4-1).

ljv6 0401
Figure 4-1. Printing emojis in the macOS Terminal app

Be careful about using such characters, though. We had to use a screenshot to make sure you could see the little cuties in jshell running on a Mac. You can use jshell to test your own system. You can put up a minimal graphical application similar to our HelloJava class from “HelloJava”. Create a JFrame, add a JLabel, and make the frame visible:

jshell> import javax.swing.*

jshell> JFrame f = new JFrame("Emoji Test")
f ==> javax.swing.JFrame[frame0 ...=true]

jshell> f.add(new JLabel("Hi \uD83D\uDE00"))
$12 ==> javax.swing.JLabel[ ...=CENTER]

jshell> f.setSize(300,200)

jshell> f.setVisible(true)

Hopefully you see the smiley, but it will depend on your system. Figure 4-2 shows the results we got when doing this exact test on macOS and Linux.

ljv6 0402
Figure 4-2. Testing emoji presentation on various systems

It’s not that you can’t use or support emoji in your applications, you just have to be aware of differences in output features. Make sure your users have a good experience wherever they are running your code.


When importing the graphical components from the Swing package, be careful to use the correct javax prefix rather than the standard java prefix. More on all things Swing in Chapter 12.


Now that we know how the text of our programs is stored, we can concentrate on what to store! Programmers often include comments in their code to help explain complex bits of logic or to provide a guide to reading the code for other programmers. (Quite often the “other programmer” is yourself several months or years later.) The text in a comment is completely ignored by the compiler. Comments have no impact on the performance or functionality of your application. As such, we are big fans of writing good comments. Java supports both C-style block comments that can span multiple lines delimited by /* and */ and C++-style line comments indicated by //:

    /*  This is a
                comment.    */

    // This is a single-line comment
    // and so // is this

Block comments have both a beginning and end sequence and can cover large ranges of text. However, they cannot be “nested,” meaning that you can’t put a block comment inside of another block comment without running afoul of the compiler. Single-line comments have only a start sequence and are delimited by the end of a line; extra // indicators inside a single line have no effect. Line comments are useful for short comments within methods; they don’t conflict with block comments. You can still wrap larger chunks of code in which the single-line comments appear with a block comment. This is often called commenting out a section of code—a common trick for debugging large applications. Since the compiler ignores all comments, you can put comments on lines or around blocks of code to see how a program behaves when that code is removed.2

Javadoc Comments

A special block comment beginning with /** indicates a doc comment. A doc comment is designed to be extracted by automated documentation generators, such as the JDK’s own javadoc program or the context-aware tooltips in many IDEs. A doc comment is terminated by the next */, just as with a regular block comment. Within the doc comment, lines beginning with @ are interpreted as special instructions for the documentation generator, giving it information about the source code. By convention, each line of a doc comment begins with a *, as shown in the following example, but this is optional. Any leading spacing and the * on each line are ignored:

 * I think this class is possibly the most amazing thing you will
 * ever see. Let me tell you about my own personal vision and
 * motivation in creating it.
 * <p>
 * It all began when I was a small child, growing up on the
 * streets of Idaho. Potatoes were the rage, and life was good...
 * @see PotatoPeeler
 * @see PotatoMasher
 * @author John 'Spuds' Smith
 * @version 1.00, 19 Nov 2022
class Potato { ... }

The javadoc command-line tool creates HTML documentation for classes by reading the source code and pulling out the embedded comments and @ tags. In this example, the tags create author and version information in the class documentation. The @see tags produce hypertext links to the related class documentation.

The compiler also looks at the doc comments; in particular, it is interested in the @deprecated tag, which means that the method has been declared obsolete and should be avoided in new programs. The compiled class includes information on any deprecated methods so the compiler can warn you whenever you use a deprecated feature in your code (even if the source isn’t available).

Doc comments can appear above class, method, and variable definitions, but some tags may not apply to all of these. For example, the @exception tag can only be applied to methods. Table 4-1 summarizes the tags used in doc comments.

Table 4-1. Doc comment tags
Tag Description Applies to


Associated class name

Class, method, or variable


Source code content

Class, method, or variable


Associated URL

Class, method, or variable


Author name



Version string



Parameter name and description



Description of return value



Exception name and description



Declares an item to be obsolete

Class, method, or variable


Notes API version when item was added


Javadoc tags in doc comments represent metadata about the source code; that is, they add descriptive information about the structure or contents of the code that is not, strictly speaking, part of the application. Some additional tools extend the concept of Javadoc-style tags to include other kinds of metadata about Java programs that are carried with the compiled code and can more readily be used by the application to affect its compilation or runtime behavior. The Java annotations facility provides a more formal and extensible way to add metadata to Java classes, methods, and variables. This metadata is also available at runtime.


The @ prefix serves another role in Java that can look similar to tags. Java supports the notion of annotations as a means of marking certain content for special treatment. You apply annotations to code outside of comments. The annotation can provide information useful to the compiler or to your IDE. For example, the @SuppressWarnings annotation causes the compiler (and often your IDE as well) to hide warnings about potential problems such as unreachable code. As you get into creating more interesting classes in “Advanced Class Design”, you may see your IDE add @Overrides annotations to your code. This annotation tells the compiler to perform some extra checks; these checks are meant to help you write valid code and catch errors before you (or your users) run your program.

You can even create custom annotations to work with other tools or frameworks. While a deeper discussion of annotations is beyond the scope of this book, we wanted you to know about them, as tags like @Overrides will show up both in our code and in examples or blog posts you might find online.

Variables and Constants

While adding comments to your code is critical to producing readable, maintainable files, at some point you have to start writing some compilable content. Programming is the art of manipulating that content. In just about every language, such information is stored in variables and constants for easier use by the programmer. Java has both. Variables store information that you plan to change and reuse over time (or information that you don’t know ahead of time, such as a user’s email address). Constants store information that is, well, constant. We’ve seen examples of both elements even in our tiny starter programs. Recall our simple graphical label from “HelloJava”:

import javax.swing.*;

public class HelloJava {
  public static void main(String[] args) {
    JFrame frame = new JFrame("Hello, Java!");
    JLabel label = new JLabel("Hello, Java!", JLabel.CENTER);
    frame.setSize(300, 300);

In this snippet, frame is a variable. We load it up in line 5 with a new instance of the JFrame class. Then we get to reuse that same instance in line 7 to add our label. We reuse the variable again to set the size of our frame in line 8 and to make it visible in line 9. All that reuse is exactly where variables shine.

Line 6 contains a constant: JLabel.CENTER. Constants contain a specific value that never changes throughout your program. Information that doesn’t change may seem like a strange thing to store—why not just use the information itself each time? Constants can be simpler to use than their data; Math.PI is probably easier to remember than the value 3.141592653589793 it represents. And since you get to select the name of the constants in your own code, another benefit is that you can describe the information in a useful way. JLabel.CENTER may seem a little opaque still, but the word CENTER at least gives you a hint about what’s happening.

Using named constants also allows for simpler changes down the road. If you code something like the maximum number of some resource you use, altering that limit is much easier if all you have to do is change the initial value given to the constant. If you use a literal number like 5, every time your code needs to check that maximum, you would have to hunt through all of your Java files to track down every occurrence of a 5 and change it as well—if that particular 5 was in fact referring to the resource limit. That type of manual search and replace is prone to error, quite above and beyond being tedious.

We’ll see more details on the types and initial values of variables and constants in the next section. As always, feel free to use jshell to explore and discover some of those details on your own! Due to interpreter limitations, you cannot declare your own top-level constants in jshell. You can still use constants defined for classes like JLabel.CENTER or define them in your own classes.

Try typing the following statements into jshell to calculate and store the area of a circle in a variable using Math.PI. This exercise also proves that reassigning constants won’t work. (And again, we have to introduce a few new concepts like assignment—putting a value into a variable—and the multiplication operator *. If these commands still feel strange, read on. We’ll go over all of the new elements in more detail throughout the rest of this chapter.)

jshell> double radius = 42.0;
radius ==> 42.0

jshell> Math.PI
$2 ==> 3.141592653589793

jshell> Math.PI = 3;
|  Error:
|  cannot assign a value to final variable PI
|  Math.PI = 3;
|  ^-----^

jshell> double area = Math.PI * radius * radius;
area ==> 5541.769440932396

jshell> radius = 6;
radius ==> 6.0

jshell> area = Math.PI * radius * radius;
area ==> 113.09733552923255

jshell> area
area ==> 113.09733552923255

Notice the compiler error when we try to set Math.PI to 3. You could change radius and even area after you declare and initialize them. But variables hold only one value at a time, so the latest calculation is the only thing that remains in the variable area.


The type system of a programming language describes how its data elements (the variables and constants we just touched on) are associated with storage in memory and how they are related to one another. In a statically typed language, such as C or C++, the type of a data element is a simple, unchanging attribute that often corresponds directly to some underlying hardware phenomenon, such as a register or a pointer value. In a dynamically typed language, such as Smalltalk or Lisp, variables can be assigned arbitrary elements and can effectively change their type throughout their lifetime. A considerable amount of overhead goes into validating what happens in these languages at runtime. Scripting languages, such as Perl, achieve ease of use by providing drastically simplified type systems in which only certain data elements can be stored in variables, and values are unified into a common representation, such as strings.

Java combines many of the best features of both statically and dynamically typed languages. As in a statically typed language, every variable and programming element in Java has a type that is known at compile time, so the runtime system doesn’t normally have to check the validity of assignments between types while the code is executing. Unlike traditional C or C++, Java also maintains runtime information about objects and uses this to allow truly dynamic behavior. Java code may load new types at runtime and use them in fully object-oriented ways, allowing casting (converting between types) and full polymorphism (combining features from multiple types). Java code may also “reflect” upon or examine its own types at runtime, allowing advanced kinds of application behavior, such as interpreters that can interact with compiled programs dynamically.

Java data types fall into two categories. Primitive types represent simple values that have built-in functionality in the language; they represent numbers, boolean (true or false) values, and characters. Reference types (or class types) include objects and arrays; they are called reference types because they “refer to” a large data type that is passed “by reference,” as we’ll explain shortly. Generics are reference types that refine an existing type while still providing compile-time type safety. For example, Java has a List class that can store a series of items. Using generics, you can create a List<String> which is a List that can only contain Strings. Or we could create a list of JLabel objects with List<JLabel>. We’ll see much more of generics in Chapter 7.

Primitive Types

Numbers, characters, and boolean values are fundamental elements in Java. Unlike some other (perhaps more pure) object-oriented languages, they are not objects. For those situations where it’s desirable to treat a primitive value as an object, Java provides “wrapper” classes. (More on this later.) The major advantage of treating primitive values as special is that the Java compiler and runtime can more readily optimize their implementation. Primitive values and computations can still be mapped down to hardware, as they always have been in lower-level languages.

An important portability feature of Java is that primitive types are precisely defined. For example, you never have to worry about the size of an int on a particular platform; it’s always a 32-bit, signed number. The “size” of a numeric type determines how big (or how precise) a value you can store. For example, the byte type is an 8-bit, signed value for storing small numbers, from -128 to 127.3 The aforementioned int type can handle most numeric needs, storing values between (roughly) +/- two billion. Table 4-2 summarizes Java’s primitive types and their capacities.

Table 4-2. Java primitive data types
Type Definition Approximate range or precision


Logical value

true or false


16-bit, Unicode character

64K characters


8-bit, signed integer

-128 to 127


16-bit, signed integer

-32,768 to 32,767


32-bit, signed integer

-2.1e9 to 2.1e9


64-bit, signed integer

-9.2e18 to 9.2e18


32-bit, IEEE 754, floating-point value

6-7 significant decimal places


64-bit, IEEE 754

15 significant decimal places


Those of you with a C background may notice that the primitive types look like an idealization of C scalar types on a 32-bit machine, and you’re absolutely right. That’s how they’re supposed to look. Java’s designers made a few changes, such as supporting 16-bit characters for Unicode and dropping ad hoc pointers. But overall, the syntax and semantics of Java primitive types derive from C.

But why have sizes at all? Again, that goes back to efficiency and optimization. The number of goals in a soccer match rarely crest the single digits—they would fit in a byte variable. The number of fans watching that match, however, would need something bigger. The total amount of money spent by all of the fans at all of the soccer matches in all of the World Cup countries would need something bigger still. By picking the right size, you give the compiler the best chance at optimizing your code, thus making your application run faster, consume fewer system resources, or both.

Some scientific or cryptographic applications require you to store and manipulate very large (or very small) numbers, and value accuracy over performance. If you need bigger numbers than the primitive types offer, you can check out the BigInteger and BigDecimal classes in the java.math package. These classes offer near-infinite size or precision. (If you want to see these big numbers in action, we use BigInteger to calculate factorial values in “Creating a custom reducer”.)

Floating-point precision

Floating-point operations in Java follow the IEEE 754 international specification, which means that the result of floating-point calculations is normally the same on different Java platforms. However, Java allows for extended precision on platforms that support it. This can introduce extremely small-valued and arcane differences in the results of high-precision operations. Most applications would never notice this, but if you want to ensure that your application produces exactly the same results on different platforms, you can use the special keyword strictfp as a class modifier on the class containing the floating-point manipulation (we cover classes in Chapter 5). The compiler then prohibits these platform-specific optimizations.

Variable declaration and initialization

You declare variables inside of methods and classes with a type name, followed by one or more comma-separated variable names. For example:

    int foo;
    double d1, d2;
    boolean isFun;

You can optionally initialize a variable with an expression of the appropriate type when you declare it:

    int foo = 42;
    double d1 = 3.14, d2 = 2 * 3.14;
    boolean isFun = true;

Variables that are declared as members of a class are set to default values if they aren’t initialized (see Chapter 5). In this case, numeric types default to the appropriate flavor of zero, characters are set to the null character (\0), and boolean variables have the value false. (Reference types also get a default value, null, but more on that soon in “Reference Types”.)

Local variables, which are declared inside a method and live only for the duration of a method call, on the other hand, must be explicitly initialized before they can be used. As we’ll see, the compiler enforces this rule, so there is no danger of forgetting.

Integer literals

Integer literals can be specified in binary (base 2), octal (base 8), decimal (base 10), or hexadecimal (base 16). Binary, octal, and hexadecimal bases are mostly used when dealing with low-level file or network data. They represent useful groupings of individual bits: 1, 3, and 4 bits, respectively. Decimal values have no such mapping, but they are much more human friendly for most numeric information. A decimal integer is specified by a sequence of digits beginning with one of the characters 1–9:

    int i = 1230;

A binary number is denoted by the leading characters 0b or 0B (zero “b”), followed by a combination of zeros and ones:

    int i = 0b01001011;            // i = 75 decimal

Octal numbers are distinguished from decimal numbers by a simple leading zero:

    int i = 01230;             // i = 664 decimal

A hexadecimal number is denoted by the leading characters 0x or 0X (zero “x”), followed by a combination of digits and the characters a–f or A–F, which represent the decimal values 10–15:

    int i = 0xFFFF;            // i = 65535 decimal

Integer literals are of type int unless they are suffixed with an L, denoting that they are to be produced as a long value:

    long l = 13L;
    long l = 13;           // equivalent: 13 is converted from type int
    long l = 40123456789L;
    long l = 40123456789;  // error: too big for an int without conversion

(The lowercase letter l will also work, but it should be avoided because it often looks like the number 1.)

When a numeric type is used in an assignment or an expression involving a “larger” type with a greater range, it can be promoted to the bigger type. In the second line of the previous example, the number 13 has the default type of int, but it’s promoted to type long for assignment to the long variable.

Certain other numeric and comparison operations also cause this kind of arithmetic promotion, as do mathematical expressions involving more than one type. For example, when multiplying a byte value by an int value, the compiler promotes the byte to an int first:

    byte b = 42;
    int i = 43;
    int result = b * i;  // b is promoted to int before multiplication

You can never go the other way and assign a numeric value to a type with a smaller range without an explicit cast, a special syntax you can use to tell the compiler exactly what type you need:

    int i = 13;
    byte b = i;        // Compile-time error, explicit cast needed
    byte b = (byte) i; // OK

The cast in the third line is the (byte) phrase before our variable i. Conversions from floating-point to integer types always require an explicit cast because of the potential loss of precision.

Last and maybe least, you can add a bit of formatting to your numeric literals by utilizing the “_” (underscore) character between digits. If you have particularly large strings of digits, you can break them up as in the following examples:

    int RICHARD_NIXONS_SSN = 567_68_0515;
    int for_no_reason = 1___2___3;
    int JAVA_ID = 0xCAFE_BABE;
    long grandTotal = 40_123_456_789L;

Underscores may only appear between digits, not at the beginning or end of a number or next to the L long integer signifier. Try out some big numbers in jshell. Notice that if you try to store a long value without the L signifier, you’ll get an error. You can see how the formatting really is just for your convenience. It is not stored; only the actual value is kept in your variable or constant:

jshell> long m = 41234567890;
|  Error:
|  integer number too large
|  long m = 41234567890;
|           ^

jshell> long m = 40123456789L;
m ==> 40123456789

jshell> long grandTotal = 40_123_456_789L;
grandTotal ==> 40123456789

Try some other examples. It can be useful to get a sense of what you find readable. It can also help you learn the kinds of promotions and castings that are available or required. Nothing like immediate feedback to drive home these subtleties!

Floating-point literals

Floating-point values can be specified in decimal or scientific notation. Floating-point literals are of type double unless they are suffixed with an f or F, denoting that they are a smaller-precision float value. And just as with integer literals, you may use the underscore character to format floating-point numbers—but again, only between digits. You can’t place them at the beginning, at the end, next to the decimal point, or next to the F signifier of the number:

    double d = 8.31;
    double e = 3.00e+8;
    float f = 8.31F;
    float g = 3.00e+8F;
    float pi = 3.1415_9265F;

Character literals

A literal character value can be specified either as a single-quoted character or an escaped ASCII or Unicode sequence, also inside single quotes:

    char a = 'a';
    char newline = '\n';
    char smiley = '\u263a';

You’ll most often deal with characters collected into a String, but there are still places where individual characters are useful. For example, if you handle keyboard input in your application, you might need to process individual key presses one char at a time.

Reference Types

In an object-oriented language like Java, you create new, complex data types from simple primitives by creating a class. Each class then serves as a new type in the language. For example, if we create a new class called Car in Java, we are also implicitly creating a new type called Car. The type of an item governs how it’s used and where it can be assigned. As with primitives, an item of type Car can, in general, be assigned to a variable of type Car or passed as an argument to a method that accepts a Car value.

A type is not just a simple attribute. Classes can have relationships with other classes and so do the types that they represent. All classes in Java exist in a parent-child hierarchy, where a child class or subclass is a specialized kind of its parent class. The corresponding types have the same relationship, where the type of the child class is considered a subtype of the parent class. Because child classes inherit all of the functionality of their parent classes, an object of the child’s type is in some sense equivalent to or an extension of the parent type. An object of the child type can be used in place of an object of the parent’s type.

For example, if you create a new class, Dog, that extends Animal, the new type, Dog, is considered a subtype of Animal. Objects of type Dog can then be used anywhere an object of type Animal can be used; an object of type Dog is said to be assignable to a variable of type Animal. This is called subtype polymorphism and is one of the primary features of an object-oriented language. We’ll look more closely at classes and objects in Chapter 5.

Primitive types in Java are used and passed “by value.” This means that when a primitive value like an int is assigned to a variable or passed as an argument to a method, its value is copied. Reference types (class types), on the other hand, are always accessed “by reference.” A reference is a handle or a name for an object. What a variable of a reference type holds is a “pointer” to an object of its type (or of a subtype, as described earlier). When you assign the reference to a variable or pass it to a method, only the reference is copied, not the object to which it’s pointing. A reference is like a pointer in C or C++, except that its type is strictly enforced. The reference value itself can’t be explicitly created or changed. You must assign an appropriate object to give a reference type variable a reference value.

Let’s run through an example. We declare a variable of type Car, called myCar, and assign it an appropriate object:4

    Car myCar = new Car();
    Car anotherCar = myCar;

myCar is a reference-type variable that holds a reference to the newly constructed Car object. (For now, don’t worry about the details of creating an object; again, we’ll cover that in Chapter 5.) We declare a second Car type variable, anotherCar, and assign it to the same object. There are now two identical references : myCar and anotherCar, but only one actual Car object instance. If we change things in the state of the Car object itself, we see the same effect by looking at it with either reference. We can see behind the scenes a little bit by trying this with jshell:

jshell> class Car {}
|  created class Car

jshell> Car myCar = new Car()
myCar ==> Car@21213b92

jshell> Car anotherCar = myCar
anotherCar ==> Car@21213b92

jshell> Car notMyCar = new Car()
notMyCar ==> Car@66480dd7

Notice the result of the creation and assignments. Here you can see that Java reference types come with a pointer value (21213b92, the right side of the @) and their type (Car, the left side of the @). When we create a new Car object, notMyCar, we get a different pointer value. myCar and anotherCar point to the same object; notMyCar points to a second, separate object.

Inferring types

Modern versions of Java have continually improved the ability to infer variable types in many situations. Starting with Java 10, you can use the var keyword in conjunction with the declaration and initiation of a variable, and allow the compiler to infer the correct type:

jshell> class Car2 {}
|  created class Car2

jshell> Car2 myCar2 = new Car2()
myCar2 ==> Car2@728938a9

jshell> var myCar3 = new Car2()
myCar3 ==> Car2@6433a2

Notice the (admittedly ugly) output when you create myCar3 in jshell. Although we did not explicitly give the type as we did for myCar2, the compiler can easily understand the correct type to use, and we do, in fact, get a Car2 object.

Passing references

Object references are passed to methods in the same way. In this case, either myCar or anotherCar would serve as equivalent arguments to some hypothetical method, called myMethod(), in our hypothetical class:


An important, but sometimes confusing, distinction is that the reference itself is a value (a memory address). That value is copied when you assign it to a variable or pass it in a method call. Given our previous example, the argument passed to a method (a local variable from the method’s point of view) is actually a third reference to the Car object, in addition to myCar and anotherCar.

The method can alter the state of the Car object through that reference by calling the Car object’s methods or altering its variables. However, myMethod() can’t change the caller’s notion of the reference to myCar: that is, the method can’t change the caller’s myCar to point to a different Car object; it can change only its own reference. This will be more obvious when we talk about methods later.

Reference types always point to objects (or null), and objects are always defined by classes. Similar to native types, if you don’t initialize an instance or class variable when you declare it, the compiler will assign it the default value of null. Also, like native types, local variables that have a reference type are not initialized by default so you must set your own value before using them. However, two special kinds of reference types—arrays and interfaces—specify the type of object they point to in a slightly different way.

Arrays in Java are an interesting kind of object automatically created to hold a collection of some other type of object, known as the base type. An individual element in the array will have that base type. (So one element of an array of type int[] will be an int, and an element of an array of type String[] will be a String.) Declaring an array implicitly creates the new class type designed as a container for its base type, as you’ll see later in this chapter.

Interfaces are a bit sneakier. An interface defines a set of methods and gives that set a corresponding type. An object that implements the methods of the interface can be referred to by that interface type, as well as its own type. Variables and method arguments can be declared to be of interface types, just like other class types, and any object that implements the interface can be assigned to them. This adds flexibility in the type system and allows Java to cross the lines of the class hierarchy and make objects that effectively have many types. We’ll cover interfaces in Chapter 5 as well.

Generic types or parameterized types, as we mentioned earlier, are an extension of the Java class syntax that allows for additional abstraction in the way classes work with other Java types. Generics allow the programmer to specialize a class without changing any of that class’s code. We cover generics in detail in Chapter 7.

A Word About Strings

Strings in Java are objects; they are therefore a reference type. String objects do, however, have some special help from the Java compiler that makes them look more like primitive types. Literal string values in Java source code, a series of characters or escape sequences between double quotes, are turned into String objects by the compiler. You can use a String literal directly, pass it as an argument to methods, or assign it to a String type variable:

    System.out.println("Hello, World...");
    String s = "I am the walrus...";
    String t = "John said: \"I am the walrus...\"";

The + symbol in Java is overloaded to work with strings as well as regular numbers. Overloading is a term used in languages that allow you to use the same method name or operator symbol when working with distinct data types. With numbers, + performs addition. With strings, + performs concatenation, which is what programmers call sticking two strings together. While Java allows arbitrary overloading of methods (more in “Method Overloading”), + is one of the few overloaded operators in Java:

    String quote = "Fourscore and " + "seven years ago,";
    String more = quote + " our" + " fathers" +  " brought...";

    // quote is now "Fourscore and seven years ago,"
    // more is now " our fathers brought..."

Java builds a single String object from the concatenated string literals and provides it as the result of the expression. (More on all things String in Chapter 8.)

Statements and Expressions

Java statements appear inside methods and classes. They describe all activities of a Java program. Variable declarations and assignments, such as those in the previous section, are statements, as are basic language structures such as if/then conditionals and loops. (More on these structures later in this chapter.) Here are a few statements in Java:

    int size = 5;
    if (size > 10)
    for (int x = 0; x < size; x++) {

Expressions produce values; Java evaluates an expression to produce a result. That result can then be used as part of another expression or in a statement. Method calls, object allocations, and, of course, mathematical expressions are examples of expressions:

    // These are all valid Java expressions
    new Object()
    42 * 64

One of the tenets of Java is to keep things simple and consistent. To that end, when there are no other constraints, evaluations and initializations in Java always occur in the order in which they appear in the code—from left to right, top to bottom. You’ll see this rule used in the evaluation of assignment expressions, method calls, and array indexes, to name a few cases. In some other languages, the order of evaluation is more complicated or even implementation dependent. Java removes this element of danger by precisely and simply defining how the code is evaluated.

This doesn’t mean you should start writing obscure and convoluted statements, however. Relying on the order of evaluation of expressions in complex ways is a bad programming habit, even when it works. It produces code that is hard to read and harder to modify.


In any program, statements perform the real magic. Statements help us implement those algorithms we mentioned at the beginning of this chapter. In fact, they don’t just help, they are precisely the programming ingredient we use; each step in an algorithm will correspond to one or more statements. Statements generally do one of four things:

  • Gather input to assign to a variable

  • Write output (to your terminal, to a JLabel, etc.)

  • Make a decision about which statements to execute

  • Repeat one or more other statements

Statements and expressions in Java appear within a code block. A code block contains a series of statements surrounded by an open curly brace ({) and a close curly brace (}). The statements in a code block can include variable declarations and most of the other sorts of statements and expressions we mentioned earlier:

    int size = 5;
    // more statements could follow...

In a sense, methods are just code blocks that take parameters and can be called by their names—for example, a hypothetical method setUpDog() might start out like this:

  setUpDog(String name) {
    int size = 5;
    // do any other setup work ...

Variable declarations are scoped in Java. They are limited to their enclosing code block—that is, you cannot see or use a variable outside of the nearest set of braces:

      // Scopes are like Vegas...
      // What's declared in a scope, stays in that scope
      int i = 5;

    i = 6;  // Compile-time error, no such variable i

In this way, you can use code blocks to arbitrarily group statements and variables. The most common use of code blocks, however, is to define a group of statements for use in a conditional or iterative statement.

if/else conditionals

One of the key concepts in programming is the notion of making a decision. “If this file exists” or “If the user has a WiFi connection” are examples of the decisions computer programs and apps make all the time. Java uses the popular if/else statement for many of these types of decisions.5 Java defines an if/else clause as follows:

    if (condition)

In English, you could read that if/else statement as “if the condition is true, perform statement1. Otherwise, perform statement2.”

The condition is a Boolean expression and must be enclosed in parentheses. A Boolean expression, in turn, is either a Boolean value (true or false) or an expression that evaluates to one of those values.6 For example, i == 0 is a Boolean expression that tests whether the integer i holds the value 0:

// filename: ch04/examples/
    int i = 0;
    // you can use i now to do other work and then
    // we can test it to see if anything has changed
    if (i == 0)
      System.out.println("i is still zero");
      System.out.println("i is most definitely not zero");

The whole of the preceding example is itself a statement and could be nested within another if/else clause. The if clause has the common functionality of taking two different forms: a “one-liner” or a block. We’ll see this same pattern with other statements like the loops discussed in the next section. If you only have one statement to execute (like the simple println() calls in the previous snippet), you can place that lone statement after the if test or after the else keyword. If you need to execute more than one statement, you use a block. The block form looks like this:

    if (condition) {
      // condition was true, execute this block
      // and so on...
    } else {
      // condition was false, execute this block
      // and so on...

Here, all the enclosed statements in the block are executed for whichever branch is taken. We can use this form when we need to do more than just print a message. For example, we could guarantee that another variable, perhaps j, is not negative:

// filename: ch04/examples/
    int j = 0;
    // you can use j now to do work like i before,
    // then make sure that work didn't drop
    // j's value below zero
    if (j < 0) {
      System.out.println("j is less than 0! Resetting.");
      j = 0;
    } else {
      System.out.println("j is positive or 0. Continuing.");

Notice that we used curly braces for the if clause, with two statements, and for the else clause, which still has a single println() call. You can always use a block if you want. But if you only have one statement, the block with its braces is optional.

switch statements

Many languages support a “one of many” conditional commonly known as a switch or case statement. Given one variable or expression, a switch statement provides multiple options that might match. And we do mean might. A value does not have to match any of the switch options; in that case nothing happens. If the expression does match a case, that branch is executed. If more than one case would match, the first match wins.

The most common form of the Java switch statement takes an integer (or a numeric type argument that can be automatically promoted to an integer type) or a string, and selects among a number of alternative, constant case branches:7

    switch (expression) {
      case constantExpression :
      [ case constantExpression :
        statement;  ]
      // ...
      [ default :
        statement;  ]

The case expression for each branch must evaluate to a different constant integer or string value at compile time. Strings are compared using the String equals() method, which we’ll discuss in more detail in Chapter 8.

You can specify an optional default case to catch unmatched conditions. When executed, the switch simply finds the branch matching its conditional expression (or the default branch) and executes the corresponding statement. But that’s not the end of the story. Perhaps counterintuitively, the switch statement then continues executing branches after the matched branch until it hits the end of the switch or a special statement called break. Here are a couple of examples:

// filename: ch04/examples/
    int value = 2;

    switch(value) {
      case 1:
      case 2:
      case 3:
    // prints both 2 and 3

Using break to terminate each branch is more common:

// filename: ch04/examples/
    int value = GOOD;

    switch (value) {
      case GOOD:
        // something good
      case BAD:
        // something bad
        // neither one
        System.out.println("Not sure");
    // prints only "Good"

In this example, only one branch—GOOD, BAD, or the default—is executed. The “keep going” behavior of switch is justified when you want to cover several possible case values with the same statement(s) without resorting to duplicating a bunch of code:

// filename: ch04/examples/
    int value = MINISCULE;
    String size = "Unknown";

    switch(value) {
      case MINISCULE:
      case TEENYWEENY:
      case SMALL:
        size = "Small";
      case MEDIUM:
        size = "Medium";
      case LARGE:
      case EXTRALARGE:
        size = "Large";

    System.out.println("Your size is: " + size);

This example effectively groups the six possible values into three cases. And this grouping feature can now appear directly in expressions. Java 12 offered switch expressions as a preview feature that was honed and made permanent with Java 14.

For example, rather than printing out the size names in the example above, we could assign our size label directly to a variable:

// filename: ch04/examples/

    int value = EXTRALARGE;

    String size = switch(value) {
      case MINISCULE, TEENYWEENY, SMALL -> "Small";
      case MEDIUM -> "Medium";
      case LARGE, EXTRALARGE -> "Large";
      default -> "Unknown";
    }; // note the semicolon! It completes the switch statement

    System.out.println("Your size is: " + size);
    // prints "Your size is Large"

Note the new “arrow” (a hyphen followed by the greater-than symbol) syntax. You still use separate case entries, but with this expression syntax, the case values are given in one comma-separated list rather than as separate, cascading entries. You then use -> between the list and the value to return. This form can make the switch expression a little more compact and (hopefully) more readable.

do/while loops

The other major concept in controlling which statement gets executed next (control flow or flow of control in programmerese) is repetition. Computers are really good at doing things over and over. Repeating a block of code is done with a loop. There are a number of different loop statements in Java. Each type of loop has advantages and disadvantages. Let’s look at these different types now.

The do and while iterative statements run as long as a Boolean expression (often referred to as the loop’s condition) returns a true value. The basic structure of these loops is straightforward:

    while (condition)
      statement; // or block

      statement; // or block
    while (condition);

A while loop is perfect for waiting on some external condition, such as getting new email:


Of course, this hypothetical wait() method needs to have a limit (typically a time limit such as waiting for one second) so that it finishes and gives the loop another chance to run. But once you do have some email, you also want to process all of the messages that arrived, not just one. Again, a while loop is perfect. You can use a block of statements inside curly braces if you need to execute more than one statement in your loop. Consider a simple countdown printer:

// filename: ch04/examples/

    int count = 10;
    while(count > 0) {
      System.out.println("Counting down: " + count);
      // maybe do other useful things
      // and decrement our count
      count = count - 1;

In this example, we use the > comparison operator to monitor our count variable. We want to keep working while the countdown is positive. Inside the body of the loop, we print out the current value of count and then reduce it by one before repeating. When we eventually reduce count to 0, the loop will halt because the comparison returns false.

Unlike while loops which test their conditions first, a do-while loop (or more often just a do loop) always executes its statement body at least once. A classic example is validating input from a user. You know you need to get some information, so you request that information in the body of the loop. The loop’s condition can test for errors. If there’s a problem, the loop will start over and request the information again. That process can repeat until your request comes back without errors and you know you have good information.

    do {
      System.out.println("Please enter a valid email: ");
      String email = askUserForEmail();
    } while (email.hasErrors());

Again, the body of a do loop executes at least once. If the user gives us a valid email address the first time, we just don’t repeat the loop.

The for loop

Another popular loop statement is the for loop. It excels at counting. The most general form of the for loop is also a holdover from the C language. It can look a little messy, but it compactly represents quite a bit of logic:

    for (initialization; condition; incrementor)
      statement; // or block

The variable initialization section can declare or initialize variables that are limited to the scope of the for body. The for loop then begins a possible series of rounds in which the condition is first checked and, if true, the body statement (or block) is executed. Following each execution of the body, the incrementor expressions are evaluated to give them a chance to update variables before the next round begins. Consider a classic counting loop:

// filename: ch04/examples/

    for (int i = 0; i < 100; i++) {
      int j = i;
      // do any other work needed

This loop will execute 100 times, printing values from 0 to 99. We declare and initialize a variable, i, to zero. We use the condition clause to see if i is less than 100. If it is, then Java executes the body of the loop. In the increment clause, we bump i up by one. (We’ll see more on the comparison operators like < and >, as well as the increment shortcut ++ in the next section, “Expressions”.) After i is incremented, the loop goes back to check the condition. Java keeps repeating these steps (condition, body, increment) until i reaches 100.

Remember that the variable j is local to the block (visible only to statements within it) and will not be accessible to the code after the for loop. If the condition of a for loop returns false on the first check (for example, if we set i to 1,000 in the initialization clause), the body and incrementor section will never be executed.

You can use multiple comma-separated expressions in the initialization and incrementation sections of the for loop. For example:

// filename: ch04/examples/

    // generate some coordinates
    for (int x = 0, y = 10; x < y; x++, y--) {
      System.out.println(x + ", " + y);
      // do other stuff with our new (x, y)...

You can also initialize existing variables from outside the scope of the for loop within the initializer block. You might do this if you wanted to use the end value of the loop variable elsewhere. This practice is generally frowned upon: it’s prone to mistakes and can make your code difficult to reason about. Nonetheless, it is legal and you may hit a situation where this behavior makes the most sense to you:

    int x;
    for(x = 0; x < someHaltingValue; x++) {
      System.out.print(x + ": ");
      // do whatever work you need ...
    // x is still valid and available
    System.out.println("After the loop, x is: " + x);

In fact, you can leave out the initialization step completely if you want to work with a variable that already has a good starting value:

    int x = 1;
    for(; x < someHaltingValue; x++) {
      System.out.print(x + ": ");
      // do whatever work you need ...

Note that you do still need the semicolon that normally separates the initialization step from the condition.

The enhanced for loop

Java’s auspiciously dubbed “enhanced for loop” acts like the foreach statement in some other languages, iterating over a series of values in an array or other type of collection:

    for (varDeclaration : iterable)

The enhanced for loop can be used to loop over arrays of any type as well as any kind of Java object that implements the java.lang.Iterable interface. (We’ll have more to say on arrays, classes, and interfaces in Chapter 5.) This includes most of the classes of the Java Collections API (see Chapter 7). Here are a couple of examples:

// filename: ch04/examples/

    int [] arrayOfInts = new int [] { 1, 2, 3, 4 };
    int total = 0;

    for(int i  : arrayOfInts) {
      total = total + i;
    System.out.println("Total: " + total);

    // ArrayList is a popular collection class
    ArrayList<String> list = new ArrayList<String>();

    for(String s : list)

Again, we haven’t discussed arrays or the ArrayList class and its special syntax in this example. What we’re showing here is the syntax of the enhanced for loop iterating over both an array and a list of string values. The brevity of this form makes it popular whenever you need to work with a collection of items.


The Java break statement and its friend continue can also be used to cut a loop or conditional statement short by jumping out of it. A break causes Java to stop the current loop (or switch) statement and skip the rest of the body. Java picks up executing the code that comes after the loop. In the following example, the while loop goes on endlessly until the watchForErrors() method returns true, triggering a break statement that stops the loop and proceeds at the point marked “after the while loop”:

    while(true) {
      if (watchForErrors())
      // No errors yet so do some work...
    // The "break" will cause execution to
    // resume here, after the while loop

A continue statement causes for and while loops to move on to their next iteration by returning to the point where they check their condition. The following example prints the numbers 0 through 9, skipping the number 5:

// filename: ch04/examples/

    for (int i = 0; i < 10; i++) {
      if (i == 5)

The break and continue statements look like those in the C language, but Java’s forms have the additional ability to take a label as an argument and jump out multiple levels to the scope of the labeled point in the code. This usage is not very common in day-to-day Java coding but may be important in special cases. Here is what that looks like:

      while (condition1) {
        // ...
          while (condition2) {
            // ...
            if (smallProblem)
              break; // Will break out of just this loop

            if (bigProblem)
              break labelOne; // Will break out of both loops
        // after labelTwo
    // after labelOne

Enclosing statements, such as code blocks, conditionals, and loops, can be labeled with identifiers like labelOne and labelTwo. In this example, a break or continue without an argument has the same effect as the earlier examples. A break causes processing to resume at the point labeled “after labelTwo“; a continue immediately causes the labelTwo loop to return to its condition test.

We could use the statement break labelTwo in the smallProblem statement. It would have the same effect as an ordinary break, but break labelOne, as seen with the bigProblem statement, breaks out of both levels and resumes at the point labeled “after labelOne.” Similarly, continue labelTwo would serve as a normal continue, but continue labelOne would return to the test of the labelOne loop. Multilevel break and continue statements remove the main justification for the much maligned goto statement in C/C++.8

There are a few Java statements we aren’t going to discuss right now. The try, catch, and finally statements are used in exception handling, as we’ll discuss in Chapter 6. The synchronized statement in Java is used to coordinate access to statements among multiple threads of execution; see Chapter 9 for a discussion of thread synchronization.

Unreachable statements

On a final note, we should mention that the Java compiler flags unreachable statements as compile-time errors. An unreachable statement is one that the compiler determines will never be called. Of course, many methods or bits of code may never actually be called in your program, but the compiler detects only those that it can “prove” are never called with some clever checking at compile time. For example, a method with an unconditional return statement in the middle of it causes a compile-time error, as does a method with a conditional that the compiler can tell will never be fulfilled:

    if (1 < 2) {
      // This branch always runs and the compiler knows it
      System.out.println("1 is, in fact, less than 2");
    } else {
      // unreachable statements, this branch never runs
      System.out.println("Look at that, seems we got \"math\" wrong.");

You have to correct the unreachable errors before you can complete the compilation. Fortunately, most instances of this error are just typos that are easily fixed. On the rare occasion that this compiler check uncovers a fault in your logic and not your syntax, you can always rearrange or delete the code that cannot be executed.


An expression produces a result, or value, when it is evaluated. The value of an expression can be a numeric type, as in an arithmetic expression; a reference type, as in an object allocation; or the special type, void, which is the declared type of a method that doesn’t return a value. In the last case, the expression is evaluated only for its side effects; that is, the work it does aside from producing a value. The compiler knows the type of an expression. The value produced at runtime will either have this type or, in the case of a reference type, a compatible (assignable) subtype. (More on that compatibility in Chapter 5.)

We’ve seen several expressions already in our example programs and code snippets. We’ll also see many more examples of expressions in the section “Assignment”.


Operators help you combine or alter expressions in various ways. They “operate” expressions. Java supports almost all standard operators from the C language. These operators also have the same precedence in Java as they do in C, as shown in Table 4-3.9

Table 4-3. Java operators
Precedence Operator Operand type Description


++, —


Increment and decrement


+, -


Unary plus and minus




Bitwise complement




Logical complement


( type )




*, /, %


Multiplication, division, remainder


+, -


Addition and subtraction




String concatenation




Left shift




Right shift with sign extension




Right shift with no extension


<, <=, >, >=


Numeric comparison




Type comparison


==, !=


Equality and inequality of value


==, !=


Equality and inequality of reference




Bitwise AND




Boolean AND




Bitwise XOR




Boolean XOR




Bitwise OR




Boolean OR




Conditional AND




Conditional OR




Conditional ternary operator





We should also note that the percent (%) operator is not strictly a modulo but a remainder, and it can have a negative value. Try playing with some of these operators in jshell to get a better sense of their effects. If you’re somewhat new to programming, it is particularly useful to get comfortable with operators and their order of precedence. You’ll regularly encounter expressions and operators even when performing mundane tasks in your code:

jshell> int x = 5
x ==> 5

jshell> int y = 12
y ==> 12

jshell> int sumOfSquares = x * x + y * y
sumOfSquares ==> 169

jshell> int explicitOrder = (((x * x) + y) * y)
explicitOrder ==> 444

jshell> sumOfSquares % 5
$7 ==> 4

Java also adds some new operators. As we’ve seen, you can use the + operator with String values to perform string concatenation. Because all integer types in Java are signed values, you can use the >> operator to perform a right-arithmetic-shift operation with sign extension. The >>> operator treats the operand as an unsigned number10 and performs a right-arithmetic-shift with no sign extension. As programmers, we don’t need to manipulate the individual bits in our variables nearly as much as we used to, so you likely won’t see these shift operators very often. If they do crop up in encoding or binary data parsing examples you read online, feel free to pop into jshell to see how they work. This type of play is one of our favorite uses for jshell!


While declaring and initializing a variable is considered a statement with no resulting value, variable assignment alone is, in fact, an expression:

    int i, j;   // statement with no resulting value
    int k = 6;  // also a statement with no result
    i = 5;      // both a statement and an expression

Normally, we rely on assignment for its side effects alone, as in the first two lines above, but an assignment can be used as a value in another part of an expression. Some programmers will use this fact to assign a given value to multiple variables at once:

    j = (i = 5);
    // both j and i are now 5

Relying on order of evaluation extensively (in this case, using compound assignments) can make code obscure and hard to read. We don’t recommend it, but this type of initialization does show up in online examples.

The null value

The expression null can be assigned to any reference type. It means “no reference.” A null reference can’t be used to reference anything, and attempting to do so generates a NullPointerException at runtime. Recall from “Reference Types” that null is the default value assigned to uninitialized class and instance variables; be sure to perform your initializations before using reference type variables to avoid that exception.

Variable access

The dot (.) operator is used to select members of a class or object instance. (We’ll talk about members in detail in the following chapters.) It can retrieve the value of an instance variable (of an object) or a static variable (of a class). It can also specify a method to be invoked on an object or class:

    int i = myObject.length;
    String s =;

A reference-type expression can be used in compound evaluations (multiple uses of the dot operation in one expression) by selecting further variables or methods on the result:

    int len =;
    int initialLen =, 10).length();

The first line finds the length of our name variable by invoking the length() method of the String object. In the second case, we take an intermediate step and ask for a substring of the name string. The substring method of the String class also returns a String reference, for which we ask the length. Compounding operations like this is also called chaining method calls. One chained selection operation that we’ve used a lot already is calling the println() method on the variable out of the System class:

    System.out.println("calling println on out");

Method invocation

Methods are functions that live within a class and may be accessible through the class or its instances, depending on the kind of method. Invoking a method means to execute its body statements, passing in any required parameter variables and possibly getting a value in return. A method invocation is an expression that results in a value. The value’s type is the return type of the method:

    System.out.println("Hello, World...");
    int myLength = myString.length();

Here, we invoked the methods println() and length() on different objects. The length() method returned an integer value; the return type of println() is void (no value). It’s worth emphasizing that println() produces output, but no value. We can’t assign that method to a variable like we did above with length():

jshell> String myString = "Hi there!"
myString ==> "Hi there!"

jshell> int myLength = myString.length()
myLength ==> 9

jshell> int mistake = System.out.println("This is a mistake.")
|  Error:
|  incompatible types: void cannot be converted to int
|  int mistake = System.out.println("This is a mistake.");
|                ^--------------------------------------^

Methods make up the bulk of a Java program. While you could write some trivial applications that exist entirely inside a lone main() method of a class, you will quickly find you need to break things up. Methods not only make your application more readable, they also open the doors to complex, interesting, and useful applications that simply are not possible without them. Indeed, look back at our graphical Hello World applications in “HelloJava”. We used several methods defined for the JFrame class.

These are simple examples, but in Chapter 5 you’ll see that it gets a little more complex when there are methods with the same name but different parameter types in the same class, or when a method is redefined in a subclass.

Statements, expressions, and algorithms

Let’s assemble a collection of statements and expressions of these different types to accomplish an actual goal. In other words, let’s write some Java code to implement an algorithm. A classic example of an algorithm is Euclid’s process for finding the greatest common denominator (GCD) of two numbers. It uses a simple (if tedious) process of repeated subtraction. We can use Java’s while loop, an if/else conditional, and some assignments to get the job done:

// filename: ch04/examples/

    int a = 2701;
    int b = 222;
    while (b != 0) {
      if (a > b) {
        a = a - b;
      } else {
        b = b - a;
    System.out.println("GCD is " + a);

It’s not fancy, but it works—and it is exactly the type of task computer programs are great at performing. This is what you’re here for! Well, you’re probably not here for the greatest common denominator of 2701 and 222 (37, by the way), but you are here to start formulating the solutions to problems as algorithms and translating those algorithms into executable Java code.

Hopefully a few more pieces of the programming puzzle are starting to fall into place. But don’t worry if these ideas are still fuzzy. This whole coding process takes a lot of practice. For one of the coding exercises in this chapter, we want you to try getting that block of code above into a real Java class inside the main() method. Try changing the values of a and b. In Chapter 8 we’ll look at converting strings to numbers, so that you can find the GCD simply by running the program again, passing two numbers as parameters to the main() method, as shown in Figure 2-10, without recompiling.

Object creation

Objects in Java are allocated with the new operator:

    Object o = new Object();

The argument to new is the constructor for the class. The constructor is a method that always has the same name as the class. The constructor specifies any required parameters to create an instance of the object. The value of the new expression is a reference of the type of the created object. Objects always have one or more constructors, though they may not always be accessible to you.

We look at object creation in detail in Chapter 5. For now, just note that object creation is also a type of expression and that the result is an object reference. A minor oddity is that the binding of new is “tighter” than that of the dot (.) selector. A popular side effect of this detail is that you can create a new object and invoke a method on it without assigning the object to a reference type variable. For example, you might need the current hour of the day—but not the rest of the information found in a Date object. You don’t need to retain a reference to the newly created date, you can simply grab the attribute you need through chaining:

jshell> int hours = new Date().getHours()
hours ==> 13

The Date class is a utility class that represents the current date and time. Here we create a new instance of Date with the new operator and call its getHours() method to retrieve the current hour as an integer value. The Date object reference lives long enough to service the getHours() method call and is then cut loose and eventually garbage-collected (see “Garbage Collection”).

Calling methods from a fresh object reference in this way is a matter of style. It would certainly be clearer to allocate an intermediate variable of type Date to hold the new object and then call its getHours() method. However, combining operations like we did to get the hours above is common. As you learn Java and get comfortable with its classes and types, you’ll probably take up some of these patterns. Until then, however, don’t worry about being “verbose” in your code. Clarity and readability are more important than stylistic flourishes as you work through this book.

The instanceof operator

You use the instanceof operator to determine the type of an object at runtime. It tests to see if an object is of the same type or a subtype of the target type. (Again, more on this class hierarchy to come!) This is the same as asking if the object can be assigned to a variable of the target type. The target type may be a class, interface, or array type. instanceof returns a boolean value that indicates whether the object matches the type. Let’s try it in jshell:

jshell> boolean b
b ==> false

jshell> String str = "something"
str ==> "something"

jshell> b = (str instanceof String)
b ==> true

jshell> b = (str instanceof Object)
b ==> true

jshell> b = (str instanceof Date)
|  Error:
|  incompatible types: java.lang.String cannot be converted to java.util.Date
|  b = (str instanceof Date)
|       ^-^

Notice the final instanceof test returns an error. With its strong sense of types, Java can often catch impossible combinations at compile time. Similar to unreachable code, the compiler won’t let you proceed until you fix the issue.

The instanceof operator also correctly reports whether the object is of the type of an array:

    if (myVariable instanceof byte[]) {
      // now we're sure myVariable is an array of bytes
      // go ahead with your array work here...

It is also important to note that the value null is not considered an instance of any class. The following test returns false, no matter what type you give to the variable:

jshell> String s = null
s ==> null

jshell> Date d = null
d ==> null

jshell> s instanceof String
$7 ==> false

jshell> d instanceof Date
$8 ==> false

jshell> d instanceof String
|  Error:
|  incompatible types: java.util.Date cannot be converted to java.lang.String
|  d instanceof String
|  ^

So null is never an “instance of” any class, but Java still tracks the types of your variables and will not let you test (or cast) between incompatible types.


An array is a special type of object that can hold an ordered collection of elements. The type of the elements of the array is called the base type of the array; the number of elements it holds is a fixed attribute called its length. Java supports arrays of all primitive types as well as reference types. To create an array with a base type of byte, for example, you could use the type byte[]. Similarly, you can create an array with the base type of String with String[].

If you have done any programming in C or C++, the basic syntax of Java arrays should look familiar. You create an array of a specified length and access the elements with the index operator, []. Unlike those languages, however, arrays in Java are true, first-class objects. An array is an instance of a special Java array class and has a corresponding type in the type system. This means that to use an array, as with any other object, you first declare a variable of the appropriate type and then use the new operator to create an instance of it.

Array objects differ from other objects in Java in three respects:

  • Java implicitly creates a special Array class type for us whenever we declare a new type of array. It’s not strictly necessary to know about this process in order to use arrays, but it will help in understanding their structure and their relationship to other objects in Java later.

  • Java lets us use the [] operator to access and assign array elements so that arrays look like many experienced programmers expect. We could implement our own classes that act like arrays, but we would have to settle for having methods such as get() and set() instead of using the special [] notation.

  • Java provides a corresponding special form of the new operator that lets us construct an instance of an array with a specified length with the [] notation or initialize it directly from a structured list of values.

Arrays make it easy to work with chunks of related information, such as the lines of text in a file, or the words in one of those lines. We use them often in examples throughout the book; you’ll see many examples of creating and manipulating arrays with the [] notation in this and coming chapters.

Array Types

An array variable is denoted by a base type followed by the empty brackets, []. Alternatively, Java accepts a C-style declaration with the brackets placed after the array name.

The following declarations are equivalent:

    int[] arrayOfInts;   // preferred
    int [] arrayOfInts;  // spacing is optional
    int arrayOfInts[];   // C-style, allowed

In each case, we declare arrayOfInts as an array of integers. The size of the array is not yet an issue because we are only declaring a variable of an array type. We have not yet created an actual instance of the array class, nor its associated storage. It’s not even possible to specify the length of an array when declaring an array type variable. The size is strictly a function of the array object itself, not the reference to it.

Arrays of reference types can be created in the same way:

    String[] someStrings;
    JLabel someLabels[];

Array Creation and Initialization

You use the new operator to create an instance of an array. After the new operator, we specify the base type of the array and its length with a bracketed integer expression. We can use this syntax to create array instances with actual storage for our recently declared variables. Since expressions are allowed, we can even do a little calculating inside the brackets:

    int number = 10;
    arrayOfInts = new int[42];
    someStrings = new String[ number + 2 ];

We can also combine the steps of declaring and allocating the array:

    double[] someNumbers = new double[20];
    Component[] widgets = new Component[12];

Array indices start with zero. Thus, the first element of someNumbers[] has index 0, and the last element has index 19. After creation, the array elements themselves are initialized to the default values for their type. For numeric types, this means the elements are initially zero:

    int[] grades = new int[30];
    // first element grades[0] == 0
    // ...
    // last element grades[19] == 0

The elements of an array of objects are references to the objects—just like individual variables they point to—but they do not actually contain instances of the objects. The default value of each element is therefore null until we assign instances of appropriate objects:

    String names[] = new String[42];
    // names[0] == null
    // names[1] == null
    // ...

This is an important distinction that can cause confusion. In many other languages, the act of creating an array is the same as allocating storage for its elements. In Java, a newly allocated array of objects actually contains only reference variables, each with the value null.11 That’s not to say that there is no memory associated with an empty array; memory is needed to hold those references (the empty “slots” in the array). Figure 4-3 illustrates the names array of the previous example.

ljv6 0403
Figure 4-3. A Java array

We build our names variable as an array of strings (String[]). This particular String[] object contains four String type variables. We have assigned String objects to the first three array elements. The fourth has the default value null.

Java supports the C-style curly braces {} construct for creating an array and initializing its elements:

jshell> int[] primes = { 2, 3, 5, 7, 7+4 };
primes ==> int[5] { 2, 3, 5, 7, 11 }

jshell> primes[2]
$12 ==> 5

jshell> primes[4]
$13 ==> 11

An array object of the proper type and length is implicitly created, and the values of the comma-separated list of expressions are assigned to its elements. Note that we did not use the new keyword or the array type here. Java infers the use of new from the assignment.

We can also use the {} syntax with an array of objects. In this case, each expression must evaluate to an object that can be assigned to a variable of the base type of the array or the value null. Here are some examples:

jshell> String[] verbs = { "run", "jump", "hide" }
verbs ==> String[3] { "run", "jump", "hide" }

jshell> import javax.swing.JLabel

jshell> JLabel yesLabel = new JLabel("Yes")
yesLabel ==> javax.swing.JLabel...

jshell> JLabel noLabel = new JLabel("No")
noLabel ==> javax.swing.JLabel...

jshell> JLabel[] choices={ yesLabel, noLabel,
   ...> new JLabel("Maybe") }
choices ==> JLabel[3] { javax.swing.JLabel ... ition=CENTER] }

jshell> Object[] anything = { "run", yesLabel, new Date() }
anything ==> Object[3] { "run", javax.swing.JLabe ... 2023 }

The following declaration and initialization statements are equivalent:

    JLabel[] threeLabels = new JLabel[3];
    JLabel[] threeLabels = { null, null, null };

Obviously, the first example is better when you have a large number of things to store. Most programmers use the curly brace initialization only when they have real objects ready to store in the array.

Using Arrays

The size of an array object is available in the public variable length:

jshell> char[] alphabet = new char[26]
alphabet ==> char[26] { '\000', '\000' ... , '\000' }

jshell> String[] musketeers = { "one", "two", "three" }
musketeers ==> String[3] { "one", "two", "three" }

jshell> alphabet.length
$24 ==> 26

jshell> musketeers.length
$25 ==> 3

length is the only accessible field of an array; it is a variable, not a method as in many other languages. Happily, the compiler tells you when you accidentally use parentheses like alphabet.length(), as everyone does now and then.

Array access in Java is just like array access in many other languages; you access an element by putting an integer-valued expression between brackets after the name of the array. This syntax works both for accessing individual, existing elements and for assigning new elements. We can get our second musketeer like this:

// remember the first index is 0!
jshell> System.out.println(musketeers[1])

The following example creates an array of JButton objects called keyPad. It then fills the array with buttons, using our square brackets and the loop variable as the index:

    JButton[] keyPad = new JButton[10];
    for (int i=0; i < keyPad.length; i++)
      keyPad[i] = new JButton("Button " + i);

Remember that we can also use the enhanced for loop to iterate over array values. Here we’ll use it to print all the values we just assigned:

    for (JButton b : keyPad)

Attempting to access an element that is outside the range of the array generates an ArrayIndexOutOfBoundsException. This is a type of RuntimeException, so you can either catch and handle it yourself if you really expect it, or ignore it, as we will discuss in Chapter 6. Here’s a taste of the try/catch syntax Java uses to wrap such potentially problematic code:

    String [] states = new String [50];

    try {
      states[0] = "Alabama";
      states[1] = "Alaska";
      // 48 more...
      states[50] = "McDonald's Land";  // Error: array out of bounds
    } catch (ArrayIndexOutOfBoundsException err) {
      System.out.println("Handled error: " + err.getMessage());

It’s a common task to copy a range of elements from one array into another. One way to copy arrays is to use the low-level arraycopy() method of the System class:

    System.arraycopy(source, sourceStart, destination, destStart, length);

The following example doubles the size of the names array from an earlier example:

    String[] tmpVar = new String [ 2 * names.length ];
    System.arraycopy(names, 0, tmpVar, 0, names.length);
    names = tmpVar;

Here we allocate and assign a temporary variable, tmpVar, as a new array, twice the size of names. We use arraycopy() to copy the elements of names to the new array. Finally, we assign a temporary array to names. If there are no remaining references to the old array of names after assigning the new array to names, the old array will be garbage-collected on the next pass.

Perhaps an easier way to accomplish the same task is to use the copyOf() or copy OfRange() methods from the java.util.Arrays class:

jshell> byte[] bar = new byte[] { 1, 2, 3, 4, 5 }
bar ==> byte[5] { 1, 2, 3, 4, 5 }

jshell> byte[] barCopy = Arrays.copyOf(bar, bar.length)
barCopy ==> byte[5] { 1, 2, 3, 4, 5 }

jshell> byte[] expanded = Arrays.copyOf(bar, bar.length+2)
expanded ==> byte[7] { 1, 2, 3, 4, 5, 0, 0 }

jshell> byte[] firstThree = Arrays.copyOfRange(bar, 0, 3)
firstThree ==> byte[3] { 1, 2, 3 }

jshell> byte[] lastThree = Arrays.copyOfRange(bar, 2, bar.length)
lastThree ==> byte[3] { 3, 4, 5 }

jshell> byte[] plusTwo = Arrays.copyOfRange(bar, 2, bar.length+2)
plusTwo ==> byte[5] { 3, 4, 5, 0, 0 }

The copyOf() method takes the original array and a target length. If the target length is larger than the original array length, then the new array is padded (with zeros or nulls) to the desired length. The copyOfRange() takes a starting index (inclusive) and an ending index (exclusive) and a desired length, which will also be padded, if necessary.

Anonymous Arrays

Often it is convenient to create throwaway arrays: arrays that are used in one place and never referenced anywhere else. Such arrays don’t need names because you never refer to them again in that context. For example, you may want to create a collection of objects to pass as an argument to some method. It’s easy enough to create a normal, named array, but if you don’t actually work with the array (if you use the array only as a holder for some collection), you shouldn’t need to name that temporary holder. Java makes it easy to create “anonymous” (unnamed) arrays.

Let’s say you need to call a method named setPets(), which takes an array of Animal objects as arguments. Provided Cat and Dog are subclasses of Animal, here’s how to call setPets() using an anonymous array:

    Dog pete = new Dog ("golden");
    Dog mj = new Dog ("black-and-white");
    Cat stash = new Cat ("orange");
    setPets (new Animal[] { pete, mj, stash });

The syntax looks similar to the initialization of an array in a variable declaration. We implicitly define the size of the array and fill in its elements using the curly brace notation. However, because this is not a variable declaration, we have to explicitly use the new operator and the array type to create the array object.

Multidimensional Arrays

Java supports multidimensional arrays in the form of arrays of other arrays. You create a multidimensional array with C-like syntax, using multiple bracket pairs, one for each dimension. You also use this syntax to access elements at various positions within the array. Here’s an example of a multidimensional array that represents a hypothetical chessboard:

    ChessPiece[][] chessBoard;
    chessBoard = new ChessPiece[8][8];
    chessBoard[0][0] = new ChessPiece.Rook;
    chessBoard[1][0] = new ChessPiece.Pawn;
    chessBoard[0][1] = new ChessPiece.Knight;
    // setup the remaining pieces

Figure 4-4 illustrates the array of arrays we create.

ljv6 0404
Figure 4-4. An array of arrays of chess pieces

Here, chessBoard is declared as a variable of type ChessPiece[][] (an array of ChessPiece arrays). This declaration implicitly creates the type ChessPiece[] as well. The example illustrates the special form of the new operator used to create a multidimensional array. It creates an array of ChessPiece[] objects and then, in turn, makes each element into an array of ChessPiece objects. We then index chessBoard to specify values for particular ChessPiece elements.

Of course, you can create arrays with more than two dimensions. Here’s a slightly impractical example:

    Color [][][] rgb = new Color [256][256][256];
    rgb[0][0][0] = Color.BLACK;
    rgb[255][255][0] = Color.YELLOW;
    rgb[128][128][128] = Color.GRAY;
    // Only 16 million to go!

We can specify a partial index of a multidimensional array to get a subarray of array type objects with fewer dimensions. In our example, the variable chessBoard is of type ChessPiece[][]. The expression chessBoard[0] is valid and refers to the first element of chessBoard, which, in Java, is of type ChessPiece[]. For example, we can populate our chessboard one row at a time:

    ChessPiece[] homeRow =  {
      new ChessPiece("Rook"), new ChessPiece("Knight"),
      new ChessPiece("Bishop"), new ChessPiece("King"),
      new ChessPiece("Queen"), new ChessPiece("Bishop"),
      new ChessPiece("Knight"), new ChessPiece("Rook")

    chessBoard[0] = homeRow;

We don’t necessarily have to specify the dimension sizes of a multidimensional array with a single new operation. The syntax of the new operator lets us leave the sizes of some dimensions unspecified. The size of at least the first dimension (the most significant dimension of the array) has to be specified, but the sizes of any number of trailing, less significant array dimensions may be left undefined. We can assign appropriate array-type values later.

We can create a simplified board of Boolean values which could hypothetically track the occupied status of a given square using this technique:

    boolean [][] checkerBoard = new boolean [8][];

Here, checkerBoard is declared and created, but its elements, the eight boolean[] objects of the next level, are left empty. With this type of initialization, checkerBoard[0] is null until we explicitly create an array and assign it, as follows:

    checkerBoard[0] = new boolean [8];
    checkerBoard[1] = new boolean [8];
    // ...
    checkerBoard[7] = new boolean [8];

The code of the previous two snippets is equivalent to:

    boolean [][] checkerBoard = new boolean [8][8];

One reason you might want to leave dimensions of an array unspecified is so that you can store arrays given to us later.

Note that because the length of the array is not part of its type, the arrays in the checkerboard do not necessarily have to be of the same length; that is, multidimensional arrays don’t have to be rectangular. Consider the “triangular” array of integers shown in Figure 4-5 where row one has one column, row two has two columns, and so on.

ljv6 0405
Figure 4-5. A triangular array of arrays

The exercises at the end of the chapter give you a chance to set up this array and initialize it yourself!

Types and Classes and Arrays, Oh My!

Java has a wide variety of types for storing information, each with its own way of representing literal bits of that information. Over time, you’ll gain familiarity and comfort with ints and doubles and chars and Strings. But don’t rush—these fundamental building blocks are exactly the kind of thing jshell was designed to help you explore. It’s always worth a moment to check your understanding of what a variable can store. Arrays in particular might benefit from a little experimentation. You can try out the different declaration techniques and confirm that you have a grasp of how to access the individual elements inside single-dimensional and multidimensional structures.

You can also play with simple flow-of-control statements in jshell like our if branching and while looping statements. It requires a little patience to type in the occasional multiline snippet, but we can’t overstate how useful play and practice are as you load more and more details of Java into your brain. Programming languages are certainly not as complex as human languages, but they still have many similarities. You can gain literacy in Java just as you have in English (or the language you’re using to read this book, if you have a translation). You will start to get a feel for what the code is meant to do even if you don’t immediately understand the particulars.

Some parts of Java, like arrays, are definitely full of particulars. We noted earlier that arrays are instances of special array classes in the Java language. If arrays have classes, where do they fit into the class hierarchy and how are they related? These are good questions, but we need to talk more about the object-oriented aspects of Java before answering them. That’s the subject of Chapter 5. For now, take it on faith that arrays fit into the class hierarchy.

Review Questions

  1. What text encoding format is used by default by Java in compiled classes?

  2. What characters are used to enclose a multiline comment? Can those comments be nested?

  3. Which looping constructs does Java support?

  4. In a chain of if/else if tests, what happens if multiple conditions are true?

  5. If you wanted to store the US stock market’s total capitalization (roughly $31 trillion at the close of fiscal year 2022) as whole dollars, what primitive data type could you use?

  6. What does the expression 18 - 7 * 2 evaluate to?

  7. How would you create an array to hold the names of the days of the week?

Code Exercises

For your coding practice, we’ll build on two of the examples from this chapter:

  1. Implement Euclid’s GCD algorithm as a full class named Euclid. Recall the basics of the algorithm:

        int a = 2701;
        int b = 222;
        while (b != 0) {
          if (a > b) {
            a = a - b;
          } else {
            b = b - a;
        System.out.println("GCD is " + a);

    For your output, can you think of a way to show the user the original values of a and b in addition to the common denominator? The ideal output would look something like this:

    % java Euclid
    The GCD of 2701 and 222 is 37
  2. Try creating the triangular array from the previous section into a simple class or in jshell. Here’s one way:

        int[][] triangle = new int[5][];
        for (int i = 0; i < triangle.length; i++) {
          triangle[i] = new int [i + 1];
          for (int j = 0; j < i + 1; j++)
            triangle[i][j] = i + j;

    Now expand that code to print the contents of triangle to the screen. To help, recall that you can print the value of an array element with the System.out.println() method:


    Your output will probably be a long, vertical line of numbers like this:


Advanced Exercises

  1. If you’re up for a bit more of a challenge, try arranging the output in a visual triangle. The statement above prints one element on a line by itself. The built-in System.out object has another output method: print(). This method does not print a newline after it prints whatever argument was passed in. You can chain together several System.out.print() calls to produce one line of output:

        System.out.print(" ");
        System.out.println(); // We do want to complete the line
        // Output:
        // Hello triangle!

    Your final output should look similar to this:

    % java Triangle
    1 2
    2 3 4
    3 4 5 6
    4 5 6 7 8

1 Check out the official Unicode site for more information. Curiously, one of the scripts listed as “obsolete and archaic” and not currently supported by the Unicode standard is Javanese—a historical language of the people of the Indonesian island of Java.

2 Using a comment to “hide” code can be safer than simply deleting the code. If you want the code back, you just take out the comment delimiter(s).

3 Java uses a technique called “two’s complement” to store integers. This technique uses one bit at the beginning of the number to determine whether it is a positive or negative value. A quirk of this technique is that the negative range is always larger by one.

4 The comparable code in C++ would be:
Car& myCar = *(new Car());
Car& anotherCar = myCar;

5 We say popular because many programming languages have this same conditional statement.

6 The word “Boolean” comes from the English mathematician, George Boole, who laid the groundwork for logic analysis. The word is rightly capitalized, but many computer languages have a “boolean” type that uses lowercase—including Java. You will invariably see both variations online.

7 We won’t be covering other forms here, but Java also supports using enumeration types and class matching in switch statements.

8 Jumping to named labels is still considered bad form.

9 You might remember the term precedence—and its cute mnemonic, “Please Excuse My Dear Aunt Sally”—from high school algebra. Java evaluates (p)arentheses first, then any (e)xponents, then (m)ultiplication and (d)ivision, and finally (a)ddition and (s)ubtraction.

10 Computers represent integers in one of two ways: signed integers, which allow negative numbers, and unsigned, which do not. A signed byte, for example, has the range -128…​127. An unsigned byte has the range 0…​255.

11 The analog in C or C++ is an array of pointers. However, pointers in C or C++ are themselves two-, four-, or eight-byte values. Allocating an array of pointers is, in actuality, allocating the storage for some number of those pointer values. An array of references is conceptually similar, although references are not themselves objects. We can’t manipulate references or parts of references other than by assignment, and their storage requirements (or lack thereof) are not part of the high-level Java language specification.

Get Learning Java, 6th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.