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:
\
uxxxx
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).
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.
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.
Warning
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.
Comments
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
multiline
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.
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 |
Class |
|
Version string |
Class |
|
Parameter name and description |
Method |
|
Description of return value |
Method |
|
Exception name and description |
Method |
|
Declares an item to be obsolete |
Class, method, or variable |
|
Notes API version when item was added |
Variable |
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.
Annotations
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
.
add
(
label
);
frame
.
setSize
(
300
,
300
);
frame
.
setVisible
(
true
);
}
}
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
.
Types
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 String
s. 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.
Type | Definition | Approximate range or precision |
---|---|---|
|
Logical value |
|
|
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 |
Note
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:
myMethod
(
myCar
);
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
)
doSomething
();
for
(
int
x
=
0
;
x
<
size
;
x
++
)
{
doSomethingElse
();
doMoreThings
();
}
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
()
Math
.
sin
(
3.1415
)
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.
Statements
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
;
setName
(
"Max"
);
// 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
;
setName
(
name
);
// 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
)
statement1
;
else
statement2
;
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/IfDemo.java
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"
);
else
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
statement
;
statement
;
// and so on...
}
else
{
// condition was false, execute this block
statement
;
statement
;
// 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/IfDemo.java
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
:
statement
;
[
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/SwitchDemo.java
int
value
=
2
;
switch
(
value
)
{
case
1
:
System
.
out
.
println
(
1
);
case
2
:
System
.
out
.
println
(
2
);
case
3
:
System
.
out
.
println
(
3
);
}
// prints both 2 and 3
Using break
to terminate each branch is more common:
// filename: ch04/examples/SwitchDemo.java
int
value
=
GOOD
;
switch
(
value
)
{
case
GOOD
:
// something good
System
.
out
.
println
(
"Good"
);
break
;
case
BAD
:
// something bad
System
.
out
.
println
(
"Bad"
);
break
;
default
:
// neither one
System
.
out
.
println
(
"Not sure"
);
break
;
}
// 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/SwitchDemo.java
int
value
=
MINISCULE
;
String
size
=
"Unknown"
;
switch
(
value
)
{
case
MINISCULE
:
case
TEENYWEENY
:
case
SMALL
:
size
=
"Small"
;
break
;
case
MEDIUM
:
size
=
"Medium"
;
break
;
case
LARGE
:
case
EXTRALARGE
:
size
=
"Large"
;
break
;
}
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/SwitchDemo.java
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
do
statement
;
// or block
while
(
condition
);
A while
loop is perfect for waiting on some external condition, such as getting new email:
while
(
mailQueue
.
isEmpty
())
wait
();
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/WhileDemo.java
int
count
=
10
;
while
(
count
>
0
)
{
System
.
out
.
println
(
"Counting down: "
+
count
);
// maybe do other useful things
// and decrement our count
count
=
count
-
1
;
}
System
.
out
.
println
(
"Done"
);
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
=
askUserForEmail
();
}
while
(
.
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/ForDemo.java
for
(
int
i
=
0
;
i
<
100
;
i
++
)
{
System
.
out
.
println
(
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/ForDemo.java
// 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
.
(
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
.
(
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
)
statement_or_block
;
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/EnhancedForDemo.java
int
[]
arrayOfInts
=
new
int
[]
{
1
,
2
,
3
,
4
};
int
total
=
0
;
for
(
int
i
:
arrayOfInts
)
{
System
.
out
.
println
(
i
);
total
=
total
+
i
;
}
System
.
out
.
println
(
"Total: "
+
total
);
// ArrayList is a popular collection class
ArrayList
<
String
>
list
=
new
ArrayList
<
String
>
();
list
.
add
(
"foo"
);
list
.
add
(
"bar"
);
for
(
String
s
:
list
)
System
.
out
.
println
(
s
);
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.
break/continue
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
())
break
;
// 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/ForDemo.java
for
(
int
i
=
0
;
i
<
10
;
i
++
)
{
if
(
i
==
5
)
continue
;
System
.
out
.
println
(
i
);
}
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:
labelOne
:
while
(
condition1
)
{
// ...
labelTwo
:
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"
);
return
;
}
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.
Expressions
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
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
Precedence | Operator | Operand type | Description |
---|---|---|---|
1 |
++, — |
Arithmetic |
Increment and decrement |
1 |
+, - |
Arithmetic |
Unary plus and minus |
1 |
~ |
Integral |
Bitwise complement |
1 |
! |
Boolean |
Logical complement |
1 |
|
Any |
Cast |
2 |
*, /, % |
Arithmetic |
Multiplication, division, remainder |
3 |
+, - |
Arithmetic |
Addition and subtraction |
3 |
+ |
String |
String concatenation |
4 |
<< |
Integral |
Left shift |
4 |
>> |
Integral |
Right shift with sign extension |
4 |
>>> |
Integral |
Right shift with no extension |
5 |
<, <=, >, >= |
Arithmetic |
Numeric comparison |
5 |
|
Object |
Type comparison |
6 |
==, != |
Primitive |
Equality and inequality of value |
6 |
==, != |
Object |
Equality and inequality of reference |
7 |
& |
Integral |
Bitwise AND |
7 |
& |
Boolean |
Boolean AND |
8 |
^ |
Integral |
Bitwise XOR |
8 |
^ |
Boolean |
Boolean XOR |
9 |
| |
Integral |
Bitwise OR |
9 |
| |
Boolean |
Boolean OR |
10 |
&& |
Boolean |
Conditional AND |
11 |
|| |
Boolean |
Conditional OR |
12 |
?: |
N/A |
Conditional ternary operator |
13 |
= |
Any |
Assignment |
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!
Assignment
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
=
myObject
.
name
;
myObject
.
someMethod
();
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
=
myObject
.
name
.
length
();
int
initialLen
=
myObject
.
name
.
substring
(
5
,
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/EuclidGCD.java
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.
Arrays
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 asget()
andset()
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.
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]) two
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
)
System
.
out
.
println
(
b
);
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.
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.
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 int
s and double
s and char
s and String
s. 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
-
What text encoding format is used by default by Java in compiled classes?
-
What characters are used to enclose a multiline comment? Can those comments be nested?
-
Which looping constructs does Java support?
-
In a chain of
if/else if
tests, what happens if multiple conditions are true? -
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?
-
What does the expression
18 - 7 * 2
evaluate to? -
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:
-
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
andb
in addition to the common denominator? The ideal output would look something like this:% java Euclid The GCD of 2701 and 222 is 37
-
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 theSystem.out.println()
method:System
.
out
.
println
(
triangle
[
3
][
1
]
);
Your output will probably be a long, vertical line of numbers like this:
0 1 2 2 3 4 3 4 5 6 4 5 6 7 8
Advanced Exercises
-
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 severalSystem.out.print()
calls to produce one line of output:System
.
out
.
print
(
"Hello"
);
System
.
out
.
print
(
" "
);
System
.
out
.
print
(
"triangle!"
);
System
.
out
.
println
();
// We do want to complete the line
// Output:
// Hello triangle!
Your final output should look similar to this:
% java Triangle 0 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.