A Brief History
Java
is firstly (and foremost) a programming language; it’s also a runtime environment (virtual machine) and has built-in libraries and technology frameworks.
Java began sometime in 1990 while James Gosling was working with (then) Sun Microsystems; he was working on a product that would make Sun an important player in the emerging Internet space. Java wasn’t released in 1990—it would take James Gosling five more years to design and create the Java language. In 1996, Java 1.0 was released.
At the time of writing, Java’s
version
is 14; it certainly has gone a long way since the 1.0 release. Table
A-1 shows the past Java versions.
Java version | Version number | Release date |
---|
JDK 1.0 (Oak) | 1.0 | January 1996 |
JDK 1.1 | 1.1 | February 1997 |
J2SE 1.2 (Playground) | 1.2 | December 1998 |
J2SE 1.3 (Kestrel) | 1.3 | May 2000 |
J2SE 1.4 (Merlin) | 1.4 | February 2002 |
J2SE 5.0 (Tiger) | 1.5 | September 2004 |
Java SE 6 (Mustang) | 1.6 | December 2006 |
Java SE 7 (Dolphin) | 1.7 | July 2011 |
Java SE 8 | 1.8 | March 2014 |
Java SE 9 | 9 | September 21, 2017 |
Java SE 10 | 10 | March 20, 2018 |
Java SE 11
| 11 | September 25, 2018 |
Java SE 12 | 12 | March 19, 2019 |
Java SE 13 | 13 | September 17, 2019 |
Java SE 14 | 14 | March 17, 2020 |
Java SE 15
| 15
| Expected in September 2020
|
There were some changes in how the Java releases were named. From versions 1.0 to 1.1, it was called JDK (Java Development Kit)
; some developers still refer to it as such. Versions 1.2 to 1.4 were called J2SE (Java 2 Standard Edition)
. Starting with version 1.5, Sun Microsystems introduced external and internal versions. The internal version was 1.5, and the external version was 5.0; you need to keep in mind that Java 1.5 and Java 5.0 are the same things, just different version names. From Java 6 onward, the naming changed to Java SE.
Major versions were released after every two years, except Java 7, which took five years after the release of Java 6, and Java 8, three years after Java 7 was released. Since Java 10, the release cadence of Java is every six months.
Setup
The JDK installer
is available for Windows, Linux, and macOS. The installer package can be downloaded from the Oracle download page for Java. Currently, the stable version of the JDK is v14.
The URL for the download page changes quite often, but it’s easy to use your favorite search engine. Look for “Oracle JDK download” and follow the link. When you get to the Java 14 download page (shown in Figure
A-1), click the links for “JDK Download” and (optionally) the “Documentation Download.”
The Java documentation
can be quite handy if your IDE can take advantage of offline documentation. IDEs like JetBrains’ IntelliJ can take you straight to the Java documentation while inspecting your code.
You must agree to the license agreement before you can download the installer.
To install the JDK on macOS, double-click the downloaded DMG file and follow the prompts. The installer takes care of updating the system path, so you don’t need to perform any action after the installation
To install the JDK on Windows, double-click the downloaded zipped file and follow the prompts. Unlike in macOS, you must perform extra configuration after the setup. You need to
- 1.
Include Java/bin in your OS system path.
- 2.
Add a CLASSPATH definition in the System Path.
If you are a Linux user, you may have seen the tarball and rpm options on the download; you may use that and install it like installing any other software on your Linux platform. The Oracle page installing Java 14 on Linux can be reached here: https://docs.oracle.com/en/java/javase/14/install/installation-jdk-linux-platforms.html#GUID-737A84E4-2EFF-4D38-8E60-3E29D1B884B8
. As I mentioned before, links on the Web can change very quickly. Still, you can easily search for “Installing Java 14 on Linux” using your favorite search engine.
Alternatively
, you may install the JDK from the repositories. This instruction applies to Debian and its derivatives, for example, Ubuntu, Mint, and so on. On a terminal window, type the following command:
sudo add-apt-repository ppa:linuxuprising/java
Enter your user password, as usual. Then, check for updates and install the script:
sudo apt-get update
sudo apt-get install oracle-java14-installer oracle-java14-set-default
When the script finishes, Java 14 would have been installed.
Syntax
Syntax
is “the arrangement of words and phrases to create well-formed sentences in a language”—according to the dictionary. In this section, we’ll look at the Java syntax; but instead of words and phrases, it has statements, expressions, blocks, variables, and so on. It’s the way we arrange these language elements that makes up a program
.
A Typical Java Program
package com.workingdev.net.javabook; ❶
import java.lang.Math; ❷
public class Hello { ❸
public static void main(String args[]) { ❹
String name = "John"; ❺
System.out.printf("Hello %s", name);
double numsqrt = Math.sqrt(8 * 8); ❻
System.out.printf("Square root of 8x8 = %f", numsqrt);
}
}
❶ | This is a package statement. A package declaration tells the compiler which folder to put the resulting executable file. Think of packages as a way to implement namespaces in Java. Not all Java programs will have package declarations (although they should), but if a program will have a package statement, it needs to appear as the very first statement in the program source file. |
❷ | This is an import statement. It’s a way for Java programs to, well, import functionalities from other libraries. It’s a way of tapping resources from other programs. In this case, we’re importing the Math library from the built-in libraries of Java. You can create your libraries and then import them from one of your programs as well. Like the package statement, the import statement is written outside the class declaration. You can have more than one import statement, as your program needs dictate. |
❸ | This is a class declaration. Classes are the fundamental building blocks of Java programs. It’s the smallest unit of code that you can compile. Except for import and package statements, everything must be written inside the class structure; by everything, I mean methods and statements. There are other constructs in Java, like enums, interfaces, and annotation types, written outside the class construct. |
❹ | This is the main method. In some other languages, methods are also called functions. A method is a named collection of Java statements. You can write many statements inside a method, and when you call that method, all the statements inside will be executed. The main method, aside from being a named collection of statements, is special; it also acts as an entry point. |
❺ | This is a Java statement; specifically
, it’s an assignment statement. A statement is a line of (complete) instruction to the compiler. There are a couple of statements in Java; this is an example of an assignment statement. You’ve seen the import and the package statements before; they’re called declaration statements. We’ll discuss the other kinds of statements in the upcoming sections. |
❻ | Java expressions are built by combining operands and operators. The operands could be variables, method calls, literals, constants, and so on—in our example, the expression 8 * 8 comprises two integer literals and the multiplication operator. An expression evaluates to a single value. A critical difference between a statement and an expression is that an expression evaluates to a value, a statement doesn’t; but remember that you can convert most expressions to a statement by simply terminating the expression with a semicolon. |
If you want to follow this code example, create a file named
Hello.java, edit it to match the contents of Listing
A-1, and then on a command line, compile it using the command:
If there are no errors, the executable file Hello.class would have been produced by the compiler. To run it, type the following command:
If you are using an IDE like IDEA, Eclipse, or NetBeans, there is usually a Run or Execute button
that’s prominently displayed on the IDE toolbar; you can use that.
Compilation Unit
Java’s compilation unit is a
class
; that means everything we write has to be inside a class. Classes are declared like this:
class is a keyword in Java; it has a special meaning to the compiler. It tells the compiler that the other tokens (the Person identifier and the pair of curly braces) are parts of a class definition and, as such, need to conform to certain rules.
Person, in the previous example, is the name of the class. It’s something that the programmers (us) would supply. The pair of curly braces marks the boundaries of the class body. Anything we write should be inside the curly braces.
Classes may contain fields and methods—methods, in turn, may contain variables inside them, but we’ll get to that later.
Comments
Everything you write in a Java source
file
will be scrutinized and evaluated by the compiler. Everything. Unless you tell the compiler to ignore certain things, like
comments. So, if you want the Java compiler to ignore some statements, comment them out, as shown in Listing
A-2.
// this will be ignored ❶
String mfirstname = "John"; // this will also be ignored ❷
/* ❸
This comment will span multiple
lines
*/
public static void main(String args[]) {
}
❶ | The compiler will ignore anything to the right of the //. |
❷ | This makes the double slash great for commenting one-liners. |
❸ | If you need to write a long comment
, one that will span multiple lines, use the forward slashes with an asterisk. |
Statements
A semicolon terminates
statements
; much like natural-language statements are terminated by a period, Java statements are punctuated by a semicolon to denote that the statement is finished. If you forget this, the compiler will complain. Here are some examples of statements:
int remainder = 15 % 12;
float quotient = 15.0 / 12.0;
System.out.println(remainder);
Keywords
Java has a set of reserved words or
keywords
. These tokens are special and have meaning to the compiler. These words determine most of the things we can do in a Java program, like defining scopes, directing the flow of program control, defining native types, handling exceptions, and so on. Table
A-2 lists all the Java keywords.
abstract
| continue
| for
| new
| switch
|
assert***
| default
| goto*
| package
| synchronized
|
boolean
| do
| if
| private
| this
|
break
| double
| implements
| protected
| throw
|
byte
| else
| import
| public
| throws
|
case
| enum****
| instanceof
| return
| transient
|
catch
| extends
| int
| short
| try
|
char
| final
| interface
| static
| void
|
class
| finally
| long
| strictfp**
| volatile
|
const*
| float
| native
| super
| while
|
* | | Not used
|
** | | Added in JDK 1.2 |
*** | | Added in JDK 1.4 |
**** | | Added in JDK 1.5 |
Identifiers
Identifiers
are the names of
variables,
classes,
packages,
interfaces, and
methods; these are names that programmers (us) supply, and as such, we have a free hand on how it goes. There are a couple of rules and guidelines to observe, like
Identifiers have to be made up of letters, numbers, underscore, and the dollar sign ($).
It has to start with a letter and can’t start with a number.
Although you can use the dollar sign, it’s probably best not to, because the Java compiler uses it to name inner classes; so best to stay away from those. This isn’t a hard rule at all; this is more a suggestion.
Identifiers also cannot be the same name as one of the Java keywords.
It’s best to make the identifiers descriptive. Ideally, their meaning and symbology should be closely related to what they hold. Again, this isn’t a hard rule, but a suggestion.
I should have mentioned this early on; better late than never, Java is case sensitive. So, the following identifiers are distinct from each
other
:
myVariable
MYVARIABLE
Myvariable
MyVariable
They may read the same, but they are syntactically different, as far as Java is concerned. So, be careful. This is a very common rookie error—only second to forgetting the semicolon.
Methods
Methods are a handy way of grouping a bunch of statements
. Ideally, these statements should work together to achieve only a single goal. The method accomplishes this goal either by producing a side effect or returning a value to the caller.
Methods are always inside classes; there are no stand-alone methods. In Java, you can’t write anything outside the class structure, except comments, imports, and package declarations.
Some programmers may know methods by another name, perhaps a function or a subroutine; they wouldn’t be wrong, but since we’re coding in Java, you need to get used to calling them methods. Listing
A-3 shows a sample code for a method.
class Person {
String name = "John Doe";
void greet() {
System.out.println("Hello %s", name);
}
}
Listing A-3Class with a method
In the sample code, greet is the name of the method, and it doesn’t return anything; that’s why its type is void. There is only one statement in the body of the method, but we could have written more.
The structure of a typical
method
is as follows. It has a
Return type—This signifies the type of data the method returns to the caller. It can be a reference type or any of the eight native types. If it doesn’t return anything, its type must be written as void. When the method’s type isn’t void, it must have a return statement somewhere in its body. If the method’s type is void, it mustn’t have a return statement.
Access modifier—This determines the method’s scope, whether you can call only from within the class (most restrictive), from outside the class but within the same package, or from anywhere (least restrictive). In our previous code example, we didn’t write any access modifier for the greet method—it doesn’t mean it doesn’t have any. Java methods (and classes and variables) have a default scope of package access; they are accessible from within the class, and outside the classes provided, the caller is from a class within the same package.
Name of the method—It’s the name you give to the method; just make it descriptive and not too long.
A pair of parentheses—This is used to enclose method parameters if you decide to pass any.
Method parameters—Parameters are a useful way to pass data to the methods. You can pass more than one value to the method, and the type of data you can pass can either be of a reference type or primitive type.
Body of the method—A pair of open and close curly braces marks the method’s body’s boundaries. Within the body, you can write the program statements
.
Packages and Imports
A package is used to group related types
. Think of it as a folder in a file directory—because that’s what it is, at its most basic. If you try to open rt.jar (if you can find it somewhere where you installed the JDK) using a ZIP utility, you’ll see that it’s merely a folder hierarchy of related classes.
Packages organize the built-in libraries (from the Java API). You can familiarize yourself more by visiting the documentation at https://docs.oracle.com/en/java/javase/14/docs/api/index.html
.
You can also use packages to organize your code. By writing a package declaration at the very top of a Java source file, you’re giving the compiler an instruction on where to put the compiled codes, like in the code sample shown in Listing
A-4.
package com.workingdev.javabook;
Listing A-4Package statement
When you compile this code, the resulting directory and file arrangement will look like the one shown in Figure
A-3.
The package statement is optional; it’s not a requirement for compilation, but it’s a good idea to organize your code via packages because it acts as a namespace. It provides organization and helps to avoid naming conflicts between identically named reference types.
If you don’t write a package statement in your source file, the compiler will put the resulting compiled class in the default package, essentially the same directory where the source file is located.
If you provide a package statement
, you need to ensure that it’s the first statement in the source file; otherwise, the compiler will complain.
Program Entry Point
A
program entry point
is a location in the program (usually a method) where the first instructions are executed. The program’s entry point depends on the kind of program; a Java command-line program’s entry point is the
public static void main() method, as shown in Listing
A-5.
public class Hello {
public static void main(String args[]) {
String name = "John";
System.out.printf("Hello %s", name);
double numsqrt = Math.sqrt(8 * 8);
System.out.printf("Square root of 8x8 = %f", numsqrt);
}
}
The main method shown here has a special signature. The keywords static, void, public, and main all need to appear exactly as shown in the preceding sample code; otherwise, it won’t be treated by the runtime as an entry point; it will be just another method. Note also that the main method takes a single argument (a String array); this is also part of the signature. This parameter receives command-line arguments that are passed to the program at runtime.
Other types of programs
may have different entry points. An Android application will usually have an Activity component as an entry point.
Data Types
Like any other imperative programming language, Java relies on its ability to create, store, and edit values. These values are manipulated and transformed via arithmetic or some other means. We can work with these values by storing them into
variables, making a
variable named storage for values or data. Consider the code snippet in the following example:
In the preceding statement,
theNum is the variable name, and
int is what we call a type. The statement is declaring a variable of type
int (short for integer). A type determines the variable’s size in memory, what kinds of operations we can do with it, and what kinds of data we can store. So, since we know that
theNum is of type int, we can do something like this:
We assigned an integer literal to the variable
theNum; this
assignment operation
is permitted because
theNum is of type
int and
1 is an integer literal; what would not be okay to do is the following:
theNum = "Hello";
theNum = "1"; // even this is not okay
The word “Hello” (in double quotes, in the previous code sample) should not be assigned to an integer variable because “Hello” is a String literal; it’s of type String. Even the second line in the previous sample isn’t okay because “1” is not an integer; it is also a String literal. Anything that you enclose in double quotes is a String literal and, hence, of type String—always remember that Java is strongly typed; it won’t allow us to do these kinds of things. It should be like for like.
At a high level, Java has two kinds of types: primitive types and reference types. Let’s deal with the primitive types first.
Primitive types
are built-in to the Java language. It’s baked right in, as opposed to a reference type, which is also called a UDT (user-defined type
) or a custom type. A reference type is something that the programmer creates to extend the capabilities of the language in terms of complexity in the data structure. There are many other differences between a reference type and a primitive one, but we’ll get to that later. For now, let’s get to the primitive types first.
Java has eight primitive or native
types
. They’re listed in Table
A-3.
Type | Size in bits | Range |
---|
byte | 8 | -128 to 127
|
short | 16 | -32,768 to 32,767
|
int | 32 | -2,147,483,648 to 2,147,483,647
|
long | 64 | -9,223,372,036,854,775,808 to
9,223,372,036,854,807
|
float | 32 | 1.23e100f, -1.23e-100f, .3f, 3.14F
|
double | 64 | 1.23456e300d, -1.23456e-300d, 1e1d
|
char | 16 | 0 - 65535 or u0000 - uFFFF
|
boolean | 1 | true, false
|
Byte
The byte is a signed 8-bit 2’s complement integer—2’s complement is a binary
thing; if it’s too computer-sciencey for you, don’t worry about it, this isn’t the main takeaway from all these. I’d like you to take away that its size is 8 bits, and the range of values you can store in it is from –128 to +127. You can calculate the range of value for a given type using the formula (-1) 2 n-1 (where n is the number of bits) to get the lower bound and 2 n-1 - 1 to get the higher bound. So, for the byte, the lower bound is (-1) 2 7—we raise 2 to the power of 7 because the number of bits in a byte is 8, remember? 2 n-1 where n is the number of bits. (-2) 7 = -128 and 2 7 - 1 = 127. This is essentially the formula on how to get the range of values for integer types.
Byte variables are declared like this:
If you declare a byte
variable
as a member variable (meaning a variable that’s not inside a method) without defining it, its default value will be zero.
byte a; // default value is 0
int
The int
is larger than the byte; it’s 32 bits. You now know how to get the range of values; you can try calculating that for yourself. Like the byte, if you declare it as a member variable without defining it, its default value will also be zero.
int variables can be declared like this:
int num = 20;
int somenum = 32767;
int bignum = 1000000;
Starting from Java 7, we can also declare int values like this:
int alsobignum = 1_000_000;
It’s still illegal to put commas as part of an integer literal, but underscores are the next best thing; it reads so much easier, don’t you think?
short
The next stop on our list of primitive data types
in Java is short.
If you want to save memory and
byte is too small, we can use the type halfway between the int and the byte. Like the int and byte, its default value is also zero when declared (but not defined) as a member variable. Short variables are declared like bytes and ints; no suffixes are needed.
long
Our last primitive data type
related to integers is long; it’s the big brother of int. It’s stored in 64 bits of memory to hold a significantly larger set of possible values. It also defaults to zero when declared and not defined as a member variable.
float
The integer types byte, short, int
, and long are the counting numbers of Java. The float and double are measuring numbers. Measuring numbers have fractional parts. This is a single-precision decimal number. This means if we get past six decimal points, this number becomes less precise and more of an estimate. Its default value is not 0; rather, it’s 0.0.
We declare floats like this:
float pi = 3.1416f; // float literals have the suffix f or F
double
The type is called double
because it’s a double-precision decimal number. It’s stored in 64 bits of memory, which means it can represent a much larger range of possible numbers than float.
We declare doubles like this:
double d = 3.13457599923384753929348D; // double literals has the suffix d or D
In the absence of a suffix, the default type of a floating-point literal is double, so you don’t have to suffix double literals.
char
A char is a 16-bit integer
representing a Unicode-encoded character. Its range is from 0 to 65,535, which in Unicode represents 'u0000' to 'uffff'.
You can declare chars like this:
char a = 65;
char b = 'a';
boolean
This is probably the simplest
; you can only store the literals true or false with this kind of data.
Overflow
The primitive types, as you have seen on the table, have size
limits
. When you assign values that are outside the acceptable range, overflow can happen. Consider the following:
byte a = 127;
a = a + 1;
System.out.println(a); // prints -128
You can probably
see by now why Java has six types for the number (byte, short, int, long, float, and double); for the sake of coding expediency, we all could have been okay with using just doubles and longs—why even bother with that distinction, why not just use a single number type for everything? One of the reasons is that not all data requires a 64-bit storage space—what a waste that would have been if we stored everything in a 64-bit space. Java allows us to exercise economy by using the least possible amount of storage for our data. It saves us from being wasteful.
Casting
With primitives, you can assign a
value
to a variable that has a wider type, for example:
int i = 10;
long l = i; // this is okay
You cannot do the reverse. A variable or value of a wider type cannot be assigned to a narrower type, like this:
long l = 10;
int i = l; // not okay
If you need to store a value of a wider type to a narrower type, you need to use the cast operator, like this:
long l = 10;
int i = (int) l; // this is okay
This is called a narrowing conversion
. We store a value of a wider type to a variable of a narrower type. This is usually dangerous because you will lose some precision in the data. Still, if you downcast like in our example, you’re essentially telling the compiler that you know what you’re doing and that you should be allowed.
You don’t need to use the cast operator in a widening conversion.
Strongly and Statically Typed
By now, you’re probably
noticing
a pattern already on how Java declares variables. We write the type first and then we write the name of the variable, like this:
Most of the time, you need to tell the compiler what the type of a variable is. The compiler needs to know ahead of time, things about the variable like how much memory to allocate for it or what kind of data is it—so that when it runs its lexical analysis, it can give us warnings or errors if we write some statements that aren’t permitted for that type of data. This is how the compiler warns us of things that can go wrong before they even go wrong.
When the compiler scans the source code, it looks for the variable declarations (among other things), and then it scans the rest of the program to see if we’re doing something to the variable that we’re not supposed to. This is some kind of static analysis because the compiler is analyzing the tokens (parts of the source code) during design time (not runtime). That’s why Java is referred to as a statically typed language. We have to declare our types ahead of time; the types of the variables need to be known during design time rather than runtime.
We also need to remember that you can’t assign a value of one type to a variable of another type, like this:
A variable
that’s declared as int cannot take in a String type. You may have used a language like JavaScript whose typing is a lot more malleable than Java; JavaScript variables don’t work the same way as they do in Java.
Reference Types
Native or primitive types are
called
because they are part of the programming language itself, pretty much like a reserved word or a literal. There is another type of data in Java called reference types; they are not part of the Java language itself, they come from libraries or user-defined types—the types that you create on your own. We’ll discuss this some more when we get to the topic of
Classes. An example statement using a reference type is given in the following snippet:
String name = "John Doe";
String is a reference type, and it comes from one of the Java libraries. There are many more reference types that we can use, such as ArrayList, Date, System, Object, Map, Queue, Set, and so on. We’ll get to some of them in the subsequent sections.
You can already build apps by simply using the primitive types of Java, but you’ll soon find out that you need a more sophisticated way to represent data, which you can do by creating classes. Classes in Java are more than just data structures because not only do they have data, they also have a behavior—a class is a collection of data clumped up together with some methods that can act on that data. It’s an oversimplification, but you get the point.
When you create an object from a class, the resulting data won’t be of primitive type; it will be of reference type. So, reference types are a result of creating objects. Let’s build a small class for the purposes of our example; Listing
A-6 shows the
code
.
class Person {
String lastname;
String firstname;
public Person(String last, String first) {
lastname = last;
firstname = first;
}
public String getName() {
return String.format("%s , %s", lastname, firstname);
}
}
To use this class, we need to create an instance of it, like this:
Person john = new Person("Doe","John");
System.out.println(john.getName());
Notice the way we created the Person object; we used the new keyword for it. This is one of the telltale signs that what you’re creating isn’t a primitive type but rather an instance of reference type.
There are many reference types built-in in Java, like Date, String, ArrayList, Math, and so on, and you can build so much more on your own. Every new class you create is a new type.
Stack and Heap
Every variable you
create
will have to be stored somewhere; either it gets stored on the stack or the heap. Another key difference between primitive and reference types is that primitive values are stored on the stack, and reference types are stored on the heap. Let’s consider the following example code:
int num = 10;
Person john = new Person("Doe", "John");
The variable
num
, since it’s a primitive type, will be stored on the stack. This means the location of the variable itself contains the value of the integer literal, as shown in Figure
A-4
.
The variable john, because it’s a reference type, doesn’t contain the actual Person object. The Person object is stored somewhere on the heap. What the variable john contains is the address of the actual Person object.
Constants
A constant
is similar to a variable; you get to declare it and assign to it. The key difference between a variable and a constant is that once you assign a value to a constant, you can never reassign a value to it later.
A constant in Java is created by using the
final keyword. The
final keyword is a special modifier, meaning of which, I think, is immediately apparent. The following snippet creates and declares a constant named
PI:
final double PI = 3.1416;
PI = 1.0; // This line won’t compile. Cannot reassign PI
System.out.printf("Value of PI = %f", PI);
If you have experience
with other programming languages, you might wonder why Java doesn’t have a const keyword—well, actually, it does, but it’s not used. The const keyword is reserved, but Java didn’t assign any semantics for it. There is no shortage of discussion on this in development forums; you can look it up if you’re inclined. The JCP (Java Community Process) topic for implementing const correctness has been closed for a long time now (around 2005); I wouldn’t hold my breath waiting for the const keyword to appear in Java 14.
Operators
A program is mostly made up of statements and expressions. What we commonly do in programs is create and transform data values; to do these things, we need to learn how to use Java’s operators.
Assignment
The assignment
operator
is the equal sign, for example:
10 is the integer literal that we’re assigning to the variable
firstNumber. In an assignment operation, the RHS (right-hand side of the operator) is assigned to the LHS (left-hand side of the operator). Here are some more examples of assignment operations:
int secondNumber = firstNumber; // variables may appear on the RHS
int thirdNumber = someFunction(); // methods may also appear on the RHS
Arithmetic
Arithmetic operators
are straightforward; they do what you expect them to do. They’re the same Math operators we’ve learned in elementary school.
Addition,
multiplication,
subtraction, and
division ( + * - and
/ ) work for the number types
byte,
short,
int,
long,
double, and
float, like in the following examples:
int a = 1 + 2;
int b = a * 10;
System.out.println(b); // prints 30
It works as expected, no surprises here.
int c = b / 4;
System.out.println(c); // prints 7
You probably expected to see the value 7.5 printed out, but both operands (
b and 4) are of type
int, and we stored the result in variable
c, which is also of type
int; perhaps if we stored the results in a
double type, we’d get the correct result. Let’s see:
double d = b / 4;
System.out.println(c); // prints 7.0
Well, it printed 7.0; it’s got a decimal portion now, but it’s still not what we expected. The principle we need to remember here is that when you perform an arithmetic operation between two
ints, the result will be of type
int. If we want to get a result with the decimal portion, we need the operands to be of type
double or
float. Let’s see the next example:
double e = b / 4.0;
System.out.println(e); // now it prints 7.5
We didn’t have to convert variable b to double or float; we simply divided b with 4.0 (a double literal). This changed the expression result’s type to double. We only needed to change one of the operands’ types to double, which got the job done.
Please keep the following
rules
in mind when constructing arithmetic expressions. When one of the operands
Is a long, the expression results in a long type
Is a float, the expression results in a float type
Is a double, the expression results in a double type
Just remember that if none of the operands is long, float, or double, the result of the operation will be of type int.
Modulo (
%) also works on the number
types
. It’s similar to the division operator, but instead of getting us the quotient, it gets us the remainder, like this example:
int rem = 15 % 12;
System.out.println(rem); // prints 3
Unary
Unary operators
work on a single operand. When used, it changes either the sign or the value of the operand.
The plus sign (
+) signifies that the number is positive—which is the default, however, so you don’t have to use this operator to indicate a positive number; that is already the default. For example, instead of writing
The unary minus (
-) negates an expression. It changes whatever sign the operand has. See this example:
int a = 5; // a is 5
int b = -a; // b is -5
int c = -b; // c is +5
The unary minus doesn’t automatically change the operand’s sign to negative; it simply reverses it. Going back to unary plus, it doesn’t do anything. Consider this example:
int a = -5; // a is -5
int b = +a; // b is -5
a = 5; // a is now 5
int c = +a; // c is 5
See, it didn’t do anything. The variable b is negative in value; even after applying the unary +, the value remained negative. One can only surmise that Java added this operator out of a need for balance, but that’s just me, feel free to search for other interpretations.
The increment operator (
++) increases the value of a variable by 1.
int counter = 0; // counter ==> 0
System.out.println(counter++); // prints 0
System.out.println(counter++); // prints 1
System.out.println(++counter); // prints 3
Note that it matters
where you put the increment operator. If you put it to the operand’s right, the result of the expression is still the original value of the operand. The increment was performed after the expression has been evaluated, not before. Contrast that when we put the increment operator to the left of the operand, the effect is more immediate.
The decrement operator (--), in contrast to the ++ operator, decreases the operand by 1.
int counter = 100;
System.out.println(--counter); // prints 99
Equality and Relational
The double equals (
==) operator is used to determine
equality
of two operands, like in this example:
if(a == b) {
// true path
}
There were three statements in the previous code snippet; the first two were assignment statements, and the other one was if statement, a branching structure. We haven’t discussed control structures yet, but you can probably follow what was going on in that code. Don’t worry too much about the details of the structure. If you could get the meaning that “if a is equal to b, then execute the statements in the true path,” you got it right. That’s precisely what it means.
Take care not to confuse single equals with double equals; the former is used to assign a value to a variable, and the latter is used to test for equality. The equality operator works on all the primitive types (byte, short, int, long, double, float, char, and boolean). You should only use the double equals operator for primitive types, never for reference types (like String). To determine equality among reference types, the .equals() method must be used. We haven’t gotten far enough on reference types, but we will, in the following sections. For now, just don’t use double equals on reference types.
The not equal (!=)
operator
is the opposite of the double equals. Instead of testing for equality, this operator returns true if the operands aren’t equal. See the following example:
int a = 1;
int b = 1;
if(a != b) {
// this will be the false path
}
else {
// this is the true path
}
The other relational operators are the
following
:
> | Greater than |
< | Less than |
>= | Greater than or equal to |
<= | Less than or equal to |
They behave exactly as you think they do. They’re the same relational operators we learned in our basic mathematics.
Logical Operators
Java has four
operators
for performing logical operations, the
AND and
OR operators and their short-circuit counterparts.
&
| Conditional AND |
&&
| Conditional AND, short circuit |
|
| Conditional OR |
||
| Conditional OR, short circuit |
Here’s an example on how to use them:
if ((a > 10) & (a < 100)) {
System.out.println("a is between 11 to 99");
}
You use logical
operators
to join conditional operations, as demonstrated in the preceding code. A couple of things to remember about logical operators are as follows:
The logical AND will return true only if all operands evaluate to true. In our example, it will only return true if the variable a is both more than 10 and less than 100.
The logical OR will return false only if all operands are false; if one of the operands evaluates to true, the whole operation evaluates to true.
The && and || operators perform Conditional AND and Conditional OR operations on two boolean expressions. These operators exhibit “short-circuiting” behavior, which means that the second operand is evaluated only if needed.
Be careful of short-circuit operators; you need to remember that short-circuiting may cause some of your program path to be ignored. Consider the sample code in Listing
A-7.
private boolean testOne() {
System.out.println("testOne");
return false;
}
private boolean testTwo() {
System.out.println("testTwo");
return true;
}
if (testOne() && testTwo()) {} // prints testOne
Listing A-7testOne and testTwo methods
The methods testOne() and testTwo() both return boolean values, and they both cause some side effects as well (printing on the console). In the example, we only see the side effect of testOne() because testTwo() was no longer evaluated; it was skipped—short-circuited, to be more precise. The short-circuit AND (&&) returned false because one of the operands is false
. It didn’t bother to evaluate testTwo() any longer.
Loops and Branches
Java statements, by default, are executed sequentially, one statement after another, until there are no more statements to execute; by then, the program stops. Java also comes with statements that change the program flow. Some statements can cause the flow to branch or fork, and some statements can cause the flow to go round in circles, like in a loop. That’s what this section is about.
If and Switch Statements
The if and switch statements
cause the program flow to fork or change directions. It does this using a test condition. If the test evaluates to true, then the program goes one way; if it’s false, it goes another way. These statements have some similarities, but they differ in form.
The if statement looks like this:
if (<expression>) {
// statement
// statement
}
The
expression in the previous code snippet is required. You cannot have an
if statement with a missing expression. The expression must also resolve to either
true or
false—it must be a
boolean expression; you can imagine that you’ll use the equality and relational operators in here, quite a lot. Here’s an example of how to use the
if statement:
import static java.lang.System.out;
public class Hello {
public static void main(String[] args) {
int a = 1;
double b = 1.0;
if (a == b) {
out.printf("%d == %f", a, b); // prints 1 == 1.000000
}
}
}
The expression will evaluate to true so that you will see the printout “1 == 1.000000”. You can compare an int value with a double value, and it will behave as you expect it to. Note that a double value is wider than an int value; in this case, Java performs some type coercion and converts the narrower type (the int, in this case) to the wider type (the double, in this case) before it performed the comparison. This widening conversion, or upcasting, is performed automatically by Java.
If you need to account for multiple pathways, you can use the
else if and
else clause. Let’s see that in an
example
.
import static java.lang.System.out;
import java.util.*;
class Hello {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
Date d = new Date();
c.setTime(d);
int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
if (dayOfWeek == 1) { out.println("Sunday");}
else if (dayOfWeek == 2) { out.println("Monday");}
else if (dayOfWeek == 3) { out.println("Tuesday"); }
else if (dayOfWeek == 4) { out.println("Wednesday"); }
else if (dayOfWeek == 5) { out.println("Thursday"); }
else if (dayOfWeek == 6) { out.println("Friday"); }
else if (dayOfWeek == 7) { out.println("Saturday"); }
else { out.println("Unknown"); } // we will never get here
}
}
The else if, like the if, also takes on a boolean expression as an argument. When the expression is true, the block immediately following the else if is executed.
The else clause, which you need to write last, is a catch-all block. When none of if and else ifs evaluates to true, the block of the else clause is executed.
Switch Statement
The
switch statement
is another branching structure that we can use. You’ll find that this structure is a bit more convenient to use than
if-then-else when you need to deal with multiple pathways; its basic form is as follows:
switch(<expression>) {
case value:
// statement
break;
case value:
// statement
break;
default:
// statement
}
where
expression is either of type
byte,
short,
char,
int,
String, or
enum. Let’s see how to use it.
import static java.lang.System.out;
import java.util.Calendar;
import java.util.Date;
class Hello {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
Date d = new Date();
c.setTime(d);
int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
String day = "";
switch(dayOfWeek) {
case 1:
day = "Sunday";
break;
case 2:
day = "Monday";
break;
case 3:
day = "Tuesday";
break;
case 4:
day = "Wednesday";
break;
case 5:
day = "Thursday";
break;
case 6:
day = "Friday";
break;
case 7:
day = "Saturday";
break;
default:
day = "Dunno";
}
out.printf("Today is %s", day);
}
// when break is encountered, program control
// goes here
}
When you use the switch statement, remember the
following
:
First match wins. How you write the case statements’ order matters. Java will try to match the first case statement, then the second, then the third, until it reaches the structure’s end. When one of these cases matches, the statements inside that particular block will be executed.
Always put a break statement on every case block. When a matching case is found, all the statements in that case’s block will run; but after that, Java will also run the statements in the remaining blocks (case blocks). That’s why you have to use the break statement; break simply instructs the runtime to get out or break out of the current control structure. That means program control will jump to the statement immediately after the switch structure.
While Loop
The while
loop
lets you run statements repeatedly—in a loop. Its structure looks like this:
while(<condition>) {
// statements
}
where
condition is a boolean expression. As long as that condition remains true, all the statements in the block will be run. So, be careful in using this. Make sure that somewhere in the while block, you have an instruction that will make the condition false at some point; otherwise, you will have a loop that doesn’t end—that’s not what you want usually. Let’s see some sample
codes
.
import static java.lang.System.out;
class Hello {
public static void main(String[] args) {
int count = 0;
while (count < 11) { ❶
out.println(count);
count++; ❷
}
}
}
❶ | The runtime evaluates the condition; if it’s true, we enter the loop’s body and run all the block statements. If the condition turns out to be false, we skip the whole while block and run the first statement immediately after it. |
❷ | Let’s increment the counter to make sure that, at some point in time, it will be either equal or greater than 11, which will make the condition false. |
A close
relative
of the while loop is the do-while loop; they’re very similar in format and function, but their key difference is the placement of the condition. Let’s see an example of the do-while loop.
import static java.lang.System.out;
class Hello {
public static void main(String[] args) {
int i = 0;
do {
out.println(i++);
} while (i < 0);
}
}
The preceding code
will still run the println statement inside the block even if it turns out to be false. Unlike in the while loop, the statements in a do-while loop are guaranteed to run at least once.
For-Loop
The for
statement
is another structure we can use for looping constructs. Its basic form is as follows:
for (<initial value>;<condition>;<increment/decrement>) {
// statements
}
That looks like a handful; let’s break down its components:
The initial value is a statement. It sets or defines the initial value of a counter.
The condition gets evaluated every time the loop completes or circles back. As long as this condition evaluates to true, all the statements in the loop block get to run.
The increment or decrement is a statement that increases or decreases the value of the counter.
These three components, the
initial value,
condition, and
increment/decrement, are separated by a semicolon. Let’s see how it looks like in code.
import static java.lang.System.out;
class Hello {
public static void main(String[] args) {
for(int count = 0; count < 11; count++) {
out.println(count);
}
}
}
Simple Application of Control Structures
Let’s build a fizzbuzz
code
. This is a popular exercise for beginners. You may see some variations of this problem in various places on the Internet, but the basic idea is to use looping and branching constructions. You need to route program logic when a number is either odd or even. Our version of this problem is broken down as follows:
- 1.
Count from 1 to 100, and as you count, assign the value to a counter variable.
- 2.
Check for the counter’s current value; if it’s exactly divisible by 3, print “fizz.”
- 3.
If the present value of counter is exactly divisible by 5, print “buzz.”
- 4.
If the counter’s current value is exactly divisible by both 3 and 5, print “fizzbuzz.”
It looks like this in code:
import static java.lang.System.out;
class FizzBuzz {
public static void main(String[] args) {
for (int i = 1; i <= 100 ; i++ ) {
if ( i % 15 == 0) {
out.printf("FizzBuzz %d
", i);
}
else if (i % 5 == 0) {
out.printf("Buzz %d
", i);
}
else if (i % 3 == 0) {
out.printf("Fizz %d
", i);
}
}
}
}
Counting from 1 to 100 can be managed by using a for-loop. It’s got a built-in counter which we can increment.
The three possible branches (fizz, buzz, or fizzbuzz) can be managed using an if-else if construction.
In the first branch, we tested if (i == 15), which means the number is divisible by both 3 and 5 (3 * 5 is 15); then, we print “fizzbuzz.”
In the second branch, if the number isn’t divisible by 3*5, is it divisible by 5 then? If yes, we print “buzz.”
If it’s not divisible by 3*5 and not divisible by 5, is it divisible by 3? If so, then we print “fizz.”
We didn’t put an else clause because we’re merely interested in numbers divisible by 3, 5, or 3 and 5; we’ll simply ignore the rest
.
The next code sample shows how to print a 5x5 multiplication table. Here’s the code:
public static void main(String[] args) {
int columns = 5;
int rows = 5;
for (int i = 1; i <= columns; i++) {
for (int j = 1; j <= rows; j++) {
System.out.printf("%d ", i * j);
}
System.out.println();
}
}
}
The code doesn’t need a lot of commentaries—one loop to generate the columns and the other loop to walk through the rows.
Arrays
An array
is a named collection of data. Like a regular variable, it contains data. It has a type, but unlike a regular variable, it can hold more than just one value; it’s nonscalar, while a regular variable is scalar.
The term
scalar
comes from mathematics, specifically linear algebra, where the term is used to differentiate a number from a vector or matrix. The way we’re using scalar here is similar. A regular variable has a one-to-one correspondence with the data it holds. A statement like this
means the variable numRooms contains only one data point, and it’s the integer value 10; numRooms is a scalar variable. Could a variable refer to more than one data point? Yes. That’s precisely the case of the array. An array variable refers to multiple data points.
Think of an array as a container of data—because that’s what it is. It stores a sequence of values of the same type. It can hold different values, but they all need to be of the same type:
int numbers[] = {1, 2, 3};
An array of ints, such as the one shown in the preceding snippet, is okay. An array of Strings like this following example:
String fruits[] = {"Apples", "Oranges", "Peaches", "Bananas"};
is also okay. It’s not okay to mix data types in an array declaration, like in the following code sample:
String mixed[] = {"John", "Doe", 5}; // mixed data type is not okay
Array Creation
To create an
array
, we need to declare a variable that points to the array; in this declaration, we need to specify the type of array, whether it’s an array of ints, bytes, Strings, and so on. The declaration looks like the following:
You’ll notice that the array variable has a pair of square brackets; that’s how we distinguish them from regular variables. By the way, square brackets don’t need to always be in the right side of the variable name; it can also be written on the left side of the variable name, like this:
But the convention of many programmers is to write the brackets to the right of the variable name. This is a matter for coding convention, and I’m sure you will adopt one for yourself sooner or later, but in our examples, I’ll write the brackets to the right side.
Now that we know how to declare array variables, we can now define the array itself. Arrays can be defined in two ways. We can use array literals, which you’ve seen in earlier examples, or use the new keyword. Let’s take a closer look at array literals.
We can construct an array literal by using a pair of curly braces, then declaring the array contents inside the curly braces separating them with commas, like this:
int arrNum[] = {35, 60, 79};
The arrNum variable now points to an array of three integers. By the way, you may have noticed that in that previous code snippet, I declared and defined the array in the same line. You can do that in arrays, just like with regular variables.
Another way to declare the same integer array is to use the
new keyword, like this:
int arrnum[] = new int[3];
The new
keyword
is followed by the type of the array,
int, in this case, then immediately followed by the bracket notation. In this example,
new int[3] means we are creating an array with three elements—elements are what we call each component of the array, and these elements are arranged in sequence, like this:
arrNum[0]
arrNum[1]
arrNum[2]
Array elements don’t start with 1; instead, they start at 0. You need to get used to this kind of numbering when working with Java arrays because this is one of the sources of programming mistakes committed by newbies. To refer to the first element of the array, you need to address its 0th element.
But our task is not done yet; we’ve simply initialized the array, but we haven’t populated it with our data yet. The array has three elements that have all been initialized to 0—this is the default value that Java used to populate the array because this is the default value for an int type. To populate the array with our data, we need to refer to the array’s individual elements.
To refer to a specific element in the array, we use the name of the array and its index, like this:
where arrNum is the name of the array, and [2] is the index number. The index number is always the length of the array – 1 because it starts at 0, not 1. In this case, index number 2 is the third (and last) element of our array.
To populate our array, we can do something like the following:
arrNum[0] = 35;
arrNum[1] = 60;
arrNum[2] = 79;
The preceding example code is functionally equivalent to the following code:
int arrNum[] = {35, 60, 79};
Both codes declared an array of ints with three elements and populated the elements with the integer values 35, 60, and 79, respectively
.
Managing Arrays
What if we want to add another element
to arrNum, how should we do that? The answer is, we cannot. An array has a fixed size; once declared, we cannot add or remove elements from it without creating another array. We can change the values of the existing elements, but we can’t reduce or increase its size; it’s one of those types in Java called immutable—once created, they cannot be modified anymore. If you need a data structure that can change its size dynamically, I’d advise you to consider the more robust classes in Java Collections, for example, ArrayList; but that is not part of our current discussion.
A data structure, like an array, has many advantages over using simple variables. Imagine for a moment that we are managing a motel system, and we need to keep track of the occupancies for each room. If we were using regular variables, how would you represent five rooms in code? Some will probably do something like the following:
int room1 = 0;
int room2 = 3;
int room3 = 2;
int room4 = 0;
int room5 = 5;
The values for each room are arbitrary; I just made them up for the purposes of this example. Our five-room motel has some occupants; room 1 and room 4 are not occupied, though. If we wanted to find the total number of occupants, you could do it like this:
int totalOccupants = room1 + room2 + room3 + room4 + room5;
System.out.printf("Total occupants is %d", totalOccupants);
This might work for a five-room motel, but what if we had 100 rooms? What then? Do we declare 100 variables? Surely, we can do better, and we can, with arrays. Let’s see how that solution works.
/*
* let's declare the array using a literal.
* in a real application, the values of the array might come
* from an interactive input system
*/
int rooms[] = {0, 3, 2, 0, 5};
int totalOccupants = 0;
for(int i = 0; i < rooms.length; i++) {
totalOccupants += rooms[i];
}
System.out.printf("Total occupants is %d", totalOccupants);
The array version
of our solution may not seem shorter than the other solution, where we used five variables to compute the total, but lines of code aren’t the point here. If we were managing 100 rooms, computing for the total would require the creation and management of 100 variables; but if we used arrays, our for-loop wouldn’t change much—the array solution scales, whether for 5 rooms or 100 or 1000.
Our for-loop solution is very straightforward; it’s textbook for-loop, but a couple of items deserve a closer look and discussion. The expression
rooms.length is aptly named because it does what you think it does. It gives you the length of the rooms array;
length is a property of the array (any array for that matter) and returns an int type. The expression
totalOccupants += rooms[i];
gets the value of whatever is in rooms[i] and then accumulates that value in the variable totalOccupants.
The use of arrays over simple variables gives your code orders of magnitude in sophistication because of our ability to refer to an array element using an expression. This makes it a perfect fit for the loop constructs of Java.
The astute reader must have caught our use of the
new keyword earlier. We use the new keyword for creating arrays because like other reference types, for example, String, the array is also a reference type; it’s an object. See it for yourself, if you run the following code:
int arrNum[] = {35, 60, 79};
int fruits[] = {"Apple", "Orange"};
System.out.println(arrNum.getClass().getName());
System.out.println(fruits.getClass().getName());
The getClass().getName() calls are a dead giveaway that arrNum and fruits are reference types.
Using the Enhanced for-loop
Looping through arrays seems like a natural fit for
for-loops
; surely, you can also use the while loop, but most developers gravitate toward for-loops for array processing. You need to be mindful, though, of the array’s index system. Remember that it’s zero-based; I know that I must have mentioned this a couple of times here already, but I can’t stress it enough. Consider the following code:
int rooms[] = {0, 3, 2, 0, 5};
int totalOccupants = 0;
for(int i = 0; i <= rooms.length; i++) {
totalOccupants += rooms[i];
}
System.out.printf("Total occupants is %d", totalOccupants);
It’s almost the same code that we did earlier, but can you spot the difference? That’s right, the second expression of the for-loop is different; now it reads i <= rooms.length instead of i < rooms.length. Why is this important? It’s because the latter expression is correct, and the former will result in an ArrayIndexOutOfBoundsException. Arrays are zero-based, so we start counting from zero, not one. If you try to access an element of the array that doesn’t exist, you’ll get an out of bounds exception.
Lucky for us, Java has an enhanced for-loop that keeps us away from out of bounds error. In Java 1.5 (Tiger), a new type of for-loop was introduced, designed to simplify arrays (and Collections) processing. When used with an array, the enhanced for-loop has the following usage:
for (type identifier : arrayName) {
// statements
}
The
type identifies the type of the array, pretty much like when we declared an array. The
identifier becomes a placeholder for each of the array elements as we iterate through the array. The
arrayName is the name of the array we want to process. So, rewriting our motel rooms example will look like this:
int rooms[] = {0, 3, 2, 0, 5};
int totalOccupants = 0;
for (int room : rooms) {
totalOccupants += room;
}
System.out.printf("Total occupants is %d", totalOccupants)
;
More on Arrays
Any nontrivial program will require things beyond what we’ve done here. To be sure, our ability to create arrays, populate them, walk through them, and get the sum is already awesome; but, real-world problems will require you to do more than these. Very quickly in your coding journey, you will encounter some use cases where you compare one array with another array and see if they are equivalent lexicographically; you may need to sort the array or find a value within the array. You can write your code to meet these functionalities, but it will not be easy, nor will they be trivial. Fortunately, Java comes with built-in libraries, so we don’t have to whip up our own. The
Arrays class in the
java.util package
is part of the Java Collection Framework that makes working with arrays much more manageable. You can learn more about
java.util.Arrays at
https://docs.oracle.com/en/java/javase/14/; just search for the Arrays class. Alternatively, if you have a locally installed JDK in your workstation and you’ve set up your PATH variable correctly to include the JDK, you can open a command line and type
This command will print the method headers of the Arrays class. You won’t be able to read the full documentation, but you’ll see an API signature for the class. Let’s explore some usage scenarios for the Arrays class.
If you want to print an array’s contents to the screen, you might be tempted to write a code like this:
String fruits[] = {"Apples","Bananas","Oranges"};
System.out.print(fruits); // prints [Ljava.lang.String;@31b7dea0
That won’t work because all it prints is the address of the array. You can quickly whip up a short for-loop that walks through the array and then print the content one by one, like this:
for(String fruit: fruits)
System.out.println(fruit);
That wasn’t so bad, and it won’t take us long to do it too; alternatively, you can just use the Arrays class like
this
:
public class ArraySample {
public static void main(String args[]) {
String fruits[] = {"Apples","Bananas","Oranges"};
System.out.println(Arrays.toString(fruits));
}
}
Just make sure to import the Arrays class before you use it.
We can sort arrays like this:
int nums[] = {0, 3, 2, 0, 5};
Arrays.sort(nums);
System.out.println(Arrays.toString(nums)); // prints [0,0,2,3,5]
If you need to compare arrays, we can do it like this:
int nums[] = {0, 3, 2, 0, 5};
int numsToo[] = {3, 2, 0, 0, 5};
Arrays.sort(nums);
Arrays.sort(numsToo);
Arrays.compare(nums, numsToo);
The
compare method returns an int value. It may return either 0, a negative number, or a positive number. Here’s what the result means:
0 is returned if the two arrays (num and numsToo) are equal and contain the same elements in the same order—this is why I sorted both arrays first.
A negative number will be returned if the first array (nums) is lexicographically less than the second array (numsToo).
A positive number will be returned if the second array (numsToo) is lexicographically less than the first array.
Reference Types
We already know
Java
has eight primitive types: the byte, short, int, long, char, float, double, and boolean. You can already accomplish quite a lot by using just these types, but you’ll accomplish so much more when you start to understand and unlock Java’s custom types. A custom type is a type that you can create on your own; they’re built using native types, like this:
class Point {
double x = 10.0;
double y = 0.0;
}
In the preceding sample code, we created a new type called Point by combining two
double types, which we aptly named
x and
y; you’ll remember from trigonometry that a point needs two coordinates, the
x-coordinate and the
y-coordinate. By the way, to create a custom type, you need to create a class—we’ll discuss classes a bit more in the following sections. To use the custom type, we need to create an instance of a class, like this:
Point objPoint = new Point();
Let’s slow down a bit and examine
what “creating an instance of a class” means. Remember, in the introduction, when I said that Java is an object-oriented language, well, what it means is that Java uses objects as a primary way to organize data and functionality.
Some programs use functions, modules, or subroutines to organize data and enable us to decompose a problem into manageable chunks—Java uses objects. You can think of objects as a data structure; that won’t be wrong, but it will be incomplete. Apart from clumping up simple types to form more complex types (like what we did in the Point example earlier), an object also clumps up methods (you’ll remember that methods in Java are called functions in other languages). So, an object is more than just a data structure; it’s an organizing entity where both data and methods are contained.
Containing both data and methods in a single entity is a big deal because it promotes cohesion. Before object-oriented programming (OOP), the data and operations (functions) were disjointed. The operations don’t know anything about the data unless you pass that same data to the operation as an argument (or God forbid, your function acted on global data). When OOP came, it became possible for both data and usage semantics (operations) to be enclosed in the same structure. Let’s see another example:
class Account {
String accountName = "John Doe";
double balance = 1000.00;
void deposit(double amount) {
// statements
}
void withdraw(double amount) {
// statements
}
}
The preceding code
shows a custom type which models a rudimentary bank account object. It defines two data points, an accountName and a balance—as you can see, it’s made up not only of native types (balance, which is a double) but also of another reference type (accountName, which is a String, which is a reference type).
The usage semantics (operations) of our Account type are
deposit() and
withdraw(); both methods expect that you pass an argument when you use them. Sample usage of this custom type could be as follows:
Account acct = new Account(); // create the object
acct.deposit(100.00);
acct.withdraw(15.00);
We passed an argument to the method withdraw to tell the Account object how much we’re withdrawing. The amount to withdraw is not intrinsic to the Account object, so there’s no way it can know how much we want to withdraw unless we tell it. The same reasoning applies to the deposit method.
To use a custom type, we first need to instantiate it using the
new keyword, like this:
Account acct = new Account();
The new keyword will create a unique instance of the Account class and return the location of that instance to our
acct variable. Once you reference the Account object, you can call its methods, like this:
acct.deposit(100.00);
acct.withdraw(15.00);
Classes
By now, you probably have a good idea of how to write classes. We’ve been using them for a couple of examples already. To create a
class
, you need to use the
class keyword, and you need to provide a body for it, like this:
where
Car is the name of the class, and the pair of curly braces encloses its body. The class’ name doesn’t need to be in proper case (capitalized first letter), but that’s just a common way of writing class names. At a minimum, this is what you need to define a class. A class definition may also include other keywords that can affect its scope. For example, you may see some class definitions like
this
:
The public keyword is an example of an access modifier; there are three of them. The other two are protected and private, but you can’t use either protected or private in our example. For top-level classes, either you put a public keyword for an access modifier or don’t put anything at all—which implies that the class has package access.
Inheritance
Java is an object-oriented
language
, and as such, it supports inheritance. Inheritance is a mechanism of reuse. When a class inherits from another class, a parent-child relationship is established. The child class inherits everything that the parent class has, except for variables or methods declared private by the parent class. Listing
A-8 shows a basic example of inheritance.
public class Employee {
void work() {
System.out.println("Working");
}
}
class Programmer extends Employee { }
class Test {
public static void main(String[] args) {
new Programmer().work(); // prints Working
}
}
Listing A-8Inheritance example
The class Employee has one method named work(). The class Programmer inherited the Employee class, and it did so using the extends keyword.
Notice that the definition of class Programmer doesn’t have any methods of its own; however, when we created an instance of Programmer and invoked its work() method, it printed “Working.” This is possible because the Programmer class inherited the work() method from its parent class (Employee).
You may have noticed that our previous examples didn’t use the extended keyword; our Hello class from earlier examples didn’t have an extended keyword. Does that mean it doesn’t have a parent? Of course not. Every class you will create in Java has a parent, which means that if a class doesn’t have an extended keyword, it will automatically extend java.lang.Object.
The
java.lang.Object
is the top-level class or the root class in the Java library. Every class in the Java library extends from Object. So, at the time of compilation, our Hello class became like this:
class Hello extends java.lang.Object {
}
For brevity, it can also be written like this:
class Hello extends Object {
}
We don’t have to write the fully qualified name of Object because the package java.lang is automatically imported in every source code. We can simply use the short name, which is Object; and because all classes in Java implicitly inherit from Object, we don’t have to write the
extends Object explicitly; we can simply write like this:
Going back to the sample code in Listing
A-8 (class Programmer and Employee), we can now say the following:
The class Employee implicitly extended java.lang.Object; we don’t have to write the extends keyword anymore.
The class Programmer explicitly extended class Employee, which means it now inherits Employee. Programmer only inherits from Object indirectly—Programmer ➤ Employee ➤ Object.
You might be wondering if class Programmer can inherit from both Object and Employee directly, instead of through an inheritance hierarchy. The answer is NO. The following code snippet is illegal in Java:
class Programmer extends Employee, Object { }
Java has a rule
on single-rooted class inheritance; that is, a class can only extend, at most, one parent class.
Constructors
A
constructor
looks a lot like a method but slightly different. A constructor (
ctor, for short) is a piece of code that’s responsible for creating instances of a class. It’s the one that creates the objects. Listing
A-9 shows an example of an Employee class with a ctor that takes a String argument.
public class Employee {
String name;
Employee(String mname) {
name = mname;
}
void work() {
System.out.println("Working");
}
}
Listing A-9Employee class
Employee(String mname) is the ctor in this example. Notice how it looks like a method; it takes on a parameter, and it has a body, just like a method. However, unlike a method, it doesn’t have a return type, it doesn’t have a return statement, and its name is the same as that of the class.
To create an Employee object with this kind of constructor, you need to pass a String argument to the constructor, like this:
Employee emp = new Employee("John Doe");
When you create a new class, constructors are optional. If you don’t write a ctor, the compiler will insert a default no-arg (no argument) ctor for your class. A no-arg ctor looks like the one in Listing
A-10.
public class Employee {
String name;
void work() {
System.out.println("Working");
}
}
Listing A-10Employee with the no-arg constructor
A no-arg
constructor
will be provided by default; that’s why you don’t have to write it explicitly. However, you need to remember that the freebie ctor from the compiler will no longer be given when you write your constructors. So, if you need the no-arg ctor on top of your other constructors, you need to provide it explicitly already, as shown in Listing
A-11.
public class Employee {
String name;
Employee(String mname) {
name = mname;
}
void work() {
System.out.println("Working");
}
}
class Programmer extends Employee {}
class Test {
public static void main(String[] args) {
new Programmer().work();
}
}
Listing A-11class Employee with constructors
In this example
, we wrote an Employee ctor that takes a String argument and another ctor that takes no argument. You can write as many constructors as you need in a class.
Overloading
As you can see in Listing A-11, the Employee class
has more than one constructor. A constructor (or a method) may appear more than once in a class definition. This is known as overloading. When constructors or methods are overloaded, they still need to be unique, and that uniqueness is determined by the constructor’s or the method’s signature. The uniqueness of a signature is determined by the number and type of parameters that a method accepts.
The constructors in our Employee class (Listing A-11) are distinguishable because one accepts a String argument, and the other doesn’t accept any argument at all. You can add more constructors to the Employee class as long as it can be parametrically unique.
Overriding
Continuing with our Employee-Programmer
example
, Programmer is already inheriting from Employee; whatever Employee has, Programmer also has. The
work() method of Programmer is precisely the same behavior as the
work() method in Employee, and for good reasons, we simply inherited it. We didn’t change anything in the implementation of work in the Programmer class. But what if we have to change some aspects of the work in the Programmer class? Well, we can, by overriding the
work() implementation in Programmer. Listing
A-12 shows the code.
public class Employee {
String name;
Employee(String mname) {
name = mname;
}
void work() {
System.out.println("Working");
}
}
class Programmer extends Employee {
@Override
void work() {
System.out.println("Writing codes");
}
}
class Test {
public static void main(String[] args) {
new Programmer().work(); // prints "Writing codes"
}
}
Listing A-12Overriding work()
When we invoke
new Programmer().work(), instead of printing “Working” (which was inherited from Employee), it will now print “Writing codes.” The programmer object will no longer use the work() method from its parent because it already has its implementation of the method now.
If you want to use still the behavior of
work() from the Employee class, then add some more behavior, you need to invoke the
work() method of Employee from the overridden
work() method of Programmer, as shown in Listing
A-13.
class Programmer extends Employee {
@Override
void work() {
super.work();
System.out.println("Writing codes");
}
}
Listing A-13Overriding work, with super
The super keyword refers to an instance of the parent class. When you invoke the Programmer’s work() method, you will now get the behavior of the parent class plus whatever you added in the override.
Strings
Whenever you enclose a series of characters in double quotes, like “this,” you are effectively using the java.lang.String class
. You don’t pay attention to this type much; I bet because it seems so natural and simple to use, but there lie some problems that trip up beginning programmers when it comes to the subject of the String—it’s deceptively simple that programmers sometimes forget that String isn’t a primitive; it’s a reference type.
String Creation
The most natural way to create a String is probably like
this
:
String name = "John Doe";
It can’t be any simpler than that. Just enclose a bunch of characters in double quotes, and you should be done. However, since this is some sort of a deep dive into the String class, we’ll look at the other ways you can work with String objects.
The String constructor is overloaded; there are plenty of ways to create a String.
String name = new String();
The preceding code creates a new String object whose length is zero. If you want to find out for yourself, you can test the following code (either in a proper source file or in a JShell):
String s = new String();
System.out.printf("length of String is %d", s.length());
// prints "length of String is 0"
The easier way to create a zero-length String is of course like this:
Another way to create a String is to pass a String literal to its constructor, like this:
String name = new String("John Doe");
You can create a String object by passing a character array into its constructor, like this:
char nameArray[] = {'J','o','h','n',' ','D','o','e'};
String name = new String(nameArray);
System.out.printf("nameArray reads %s", name);
I don’t think you’ll encounter a situation where you actually create a String like the sample I showed here, but there might be an occasion in your coding life where you have to read from a network I/O or a file (or an API) where what you have is an array of character, and you need to convert it to a String. If you ever are in that situation, you now know how to build Strings from character arrays.
You can also create a String from a byte array, like this:
byte byteArray[] = {65,66,67,68,69,70};
String nameToo = new String(byteArray);
System.out.printf("nameToo reads %s", nameToo);
Again, when will you use this? If you
find yourself in a situation where you are given a byte array—very common when working with Java I/O APIs—this is how you create a String from that.
Strings Are Immutable
Immutable objects
are objects that once you create, you can no longer modify its contents. Strings are like that. Once you create a String, you won’t be able to change its contents anymore.
You might wonder, if Strings are truly immutable, then why can I do things like this:
String fruit = "Apple";
fruit = "Orange";
Surely, if I can change the value of the String variable
fruit, then I am able to modify the original “Apple” String object. Isn’t that so? Well, no. What Java did, in this case, was to create a completely different String object (“Orange”) and assigned it to the variable
fruit. Every time you try to change the content of a String, the Java runtime will create a completely new String. Consider the following code:
String empName = "John"; // (1)
empName += " "; // (2)
empName += "Doe"; // (3)
(1) Created a new String from the literal “John.”
(2) Copied the contents of the empName variable (currently contains “John”), then appended a space and created a second String object, the content of which is “John” (with empty space); then this new object was assigned to empName, and now the empName variable points to “John ” (length = 5) and no longer to “John” (length = 4).
(3) At this point, we take the content of empName (“John ”, length = 5) and append “Doe”. Same thing as number 2, the runtime copies “John ”, adds “Doe”, and reassigns it to variable empName. Now, empName points to “John Doe”. The previous String objects “John” and “John ” are now orphan objects, which makes them a candidate for garbage collection.
This is a very common operation. Accumulating String by using the addition operator is a very common technique among developers, and there’s nothing wrong with this concatenation if you won’t be creating too many objects. Be extra careful with String concatenation when you’re inside loops, because things can go very wrong (and very fast) inside loops. Consider the following snippet which reads values from a database table and then concatenates the results to a String variable:
Statement stmt=con.createStatement();
ResultSet rs=stmt.executeQuery("select * from emp");
String result = "";
while(rs.next()){
result += rs.getString(1) + " " + rs.getString(2);
}
System.out.println(result);
con.close();
If the table
has tens of thousands of rows, this code will be a source of performance problems. It can drag your app to a screeching halt as you run it. Remember that Strings are immutable, and every time you do a concatenation, Java will create a new String object. If the table has 100K rows, the code will easily create 200K String objects by the time the while loop has exited. These will be 200K orphaned objects waiting for the garbage collector (GC). The garbage collector’s schedule cannot be predicted, and by the time the GC sweeps the heap, there might not be enough memory left for your app.
If you really need to concatenate Strings inside loops, it will be better to use either the StringBuilder or the StringBuffer class. These classes are mutable, unlike the String class. Let’s rewrite our database code using the StringBuffer.
String result = "";
StringBuffer sb = new StringBuffer();
while(rs.next()){
sb.append(rs.getString(1));
sb.append(" ");
sb.append(rs.getString(2));
}
result = new String(sb);
System.out.println(result);
con.close();
The
append() method
of StringBuffer lets you add new String objects to the buffer; but unlike the String object, the StringBuffer does not create another StringBuffer object, it modifies its internal structure to accommodate the new content. Later on, you can convert the StringBuffer to a String object by passing the StringBuffer to the String’s constructor, like this:
Why Can’t We Modify Strings
If you peek at the source code of
java.lang.String
, you’ll find the following declarations:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
private final char[] value;
// LOTS of other statements
}
It implements a couple of interfaces, which are interesting, but not part of our current discussion, so we won’t get distracted by that. Notice that the class is final, which means you cannot subclass or extend it. What’s more important to notice is the member variable named value. It’s an array, and it’s declared as final. This array is used to store the characters of the String value. Arrays are fixed; once you create them, you can no longer add any more elements. Arrays cannot grow in size after they have been defined. This is the main reason why you cannot modify the contents of a String without creating a copy of the original.
Comparing Strings
Strings are reference
types
, which means they are stored on the heap; so, when comparing them with one another, you generally cannot use the equality operator ==. To compare Strings, you should use the
.equals() method
, like this:
String str1 = "Hello";
String str2 = "Hello";
if(str1.equals(str2)) {
System.out.println("str1 is equal to str2");
}
However, you may see some programmers use the == to compare Strings like this:
String str1 = "Hello";
String str2 = "Hello";
if(str1 == str2) {
System.out.println("str1 is equal to str2");
}
And it actually works. So, what’s going on? This is a special case for the String object. The rule you should follow when comparing Strings for lexical equivalence is to always use the .equals() method
. The == operator is what you use for comparing primitive values, not object types, but why is it working in the previous code example? The answer is because of the String constant pool (other programmers call it String literal pool or simply String pool; these terms may be used interchangeably).
The String constant
pool
is a special area in the heap memory that the runtime uses to create String literals, and once a String literal is created in this area, it can be reused so as to avoid duplicate copies of the same literal. Let’s see that in code.
String str1 = "Good morning"; // (1)
String str2 = "Good morning"; // (2)
if (str1 == str2) {
System.out.println("str1 == str2");
}
(1) The runtime creates a new String object and places it in the String constant pool.
(2) When str2 is declared and defined, the runtime will search the String constant pool if this literal has been created already; and it is, so Java won’t create another “Good morning” String object; instead, the address of the existing “Good morning” String will be assigned to the str2 variable. Now, str1 and str2 point to exactly the same address. This is why when you test them using double equals, the expression returns true.
An important point to remember about the String constant pool is that String literals end up in the String constant pool only when String literals are plainly assigned to a variable, like this:
String str2 = "Good morning";
When you create String objects using constructors (with the
new keyword), the String objects don’t end up in the constant pool. Let’s consider the following code:
String str1 = "Good morning"; // (1)
String str2 = new String("Good morning"); // (2)
if (str1 == str2) { // (3)
System.out.println("str1 == str2");
}
(1) This gets stored on the String constant pool; str1 points to “Good morning” in the constant pool.
(2) This gets stored on the heap like all other objects; str2 points to “Good morning” in the regular heap, not in the constant pool.
(3) str1 contains the address of an object in the constant pool; str2 contains the address of a completely different object in the heap memory. This happened because we used the new keyword in creating the String object. Whenever you use the new keyword, it creates a new object in the heap.
If you want to compare
Strings
without care or regard for case sensitivity, you can use the
.equalsIgnoreCase() method
, like this:
String str1 = "Hello";
String str2 = "hello";
if(str1.equalsIgnoreCase(str2)) {
System.out.println("str1 is equal to str2");
}
Common Usage
charAt(int indexPosition)
You can use this method to retrieve a character at a specified index of the
String
. Take note that ordinal positions of characters in a String start at zero, not one. Remember that a char array is used within the internal mechanism of the String class; and Java arrays are zero-based. Let’s look at an example:
String name = new String("Paul");
System.out.println(name.charAt(0)); // prints P
System.out.println(name.charAt(2)); // prints u
indexOf(String arg)
The
indexOf() method
takes either a character or String arguments and searches the String for any match. When a match is found, the ordinal position of the argument in the String is returned, for example:
String letters = "ABCAB";
System.out.println(letters.indexOf('B')); // prints 1
System.out.println(letters.indexOf("S")); // prints -1
System.out.println(letters.indexOf("CA")); // prints 2
When a match is not found, -1 is returned.
substring(int position)
The
substring() method
takes an integer argument, which it uses to mark the beginning position from where to extract the substring. If you don’t pass a second argument to this method, then it will return all the strings from the beginning position (marked by the argument) up to the last character of the String, for example:
String exam = "Oracle";
String sub = exam.substring(1);
System.out.println(sub); // racle
You can also specify the ending position of the substring to extract by passing the second integer argument, like this:
String exam = "Oracle";
String result = exam.substring(2, 4);
System.out.println(result); // prints ac
trim()
The
trim() method
returns a new String object, but all the whitespaces (leading or trailing) are removed.
String withSpaces = " AB CB ";
String trimmed = withSpaces.trim(); // returns "AB CB"
Note that the space in between AB and CB has not been trimmed. The trim function only removes leading and trailing whitespaces; it doesn’t remove the ones in between.
length()
This method returns the number of characters in a String. Please don’t confuse the String’s
length() method with the array’s
length property.
String withSpaces = " AB CB ";
System.out.printf("Length of %s is %d", withSpaces, withSpaces.length()); // prints 7
startsWith() and endsWith()
Both these methods take an argument, and they match those arguments against the series of characters in the String. In the case of startsWith(), if the argument passed to is a match to the characters in the String beginning at position 0, then the method returns true. See this example:
String letters = "ABCAB";
boolean a = letters.startsWith("AB")); // true
boolean b = letters.startsWith("a")); // false
boolean c = letters.endsWith("CAB") // true
boolean d = letters.endsWith("B") // true
boolean e = letters.endsWith("b") // false
replace()
This method takes two
arguments
:
The first argument is the pattern to match. The method tries to find all the occurrences of this pattern within the String.
The second argument is the replacement String; if the method finds a match for the first argument, all matching string will be replaced by the second argument. Think of it as a find-and-replace functionality in your editor.
String letters = "ABCAB";
String result letters.replace('B', 'b'); // returns AbCAb
You can also use Strings as arguments, like this:
String letters = "ABCAB";
String result = letters.replace("CA", "12"); // returns AB12B
Exceptions
Despite our best efforts to get the program
right and behave as predictably as possible, they can still fail because of various reasons. When programs fail because of abnormal conditions in their environment, the Java runtime throws an exception. When an exception is thrown, the normal flow of the program is disrupted, and if the exception is not properly handled, it may cause the program to terminate in an ungraceful manner. There are two ways to handle exceptions, either we handle it using a try-catch structure, or we rethrow it and let it be somebody else’s problem. In this section, we will look at how to handle exceptions using the try-catch block.
The general form of the try-catch is as follows:
try {
// statement that can throw exceptions
}
catch(ExceptionType1 obje) {
// error handling statement;
}
catch(ExceptionType2 obje) {
// error handling statement;
}
catch(ExceptionTypen obje) {
// error handling statement;
}
The try-catch, like the if-else, routes the program flow. It branches program control when some conditions become true. When an exception is raised inside the body of the try block, the program flow automatically jumps out of that block. The catch blocks will be inspected one by one until a type of exception thrown is properly matched to any of the catch blocks. When a match is found, the error handling statements on the matching catch block are executed. Let’s see how that looks on real code.
String filename = "something.txt";
try {
java.io.FileReader reader = new java.io.FileReader(filename);
}
catch(FileNotFoundException e) {
// ask the user to input another filename
}
In the preceding
example
, we only have one catch block; that’s because the statement inside the try block can only throw a “FileNotFoundException” and nothing else. If we had other statements in the try block that can throw other kinds of exceptions, then we should write the corresponding catch blocks for those. You might ask, “how do we know if an exception is going to be thrown by a method call?”. The answer to that question is “by reading the documentation.” If you read the Java language API reference for the FileReader class, you will learn the details of how it is used and what kinds of exceptions some of its methods may throw, among other things. Another way you may find out the kinds of exceptions that can be thrown by the FileReader (or any other method/constructor call) is to simply write it like a regular statement, that is, without any error handling structure, like so.
java.io.FileReader reader = new java.io.FileReader(filename);
As soon as you try to compile the source program where this line is written, you will find out that the FileReader constructor may throw a “FileNotFoundException” because the Java compiler will complain loudly.
The try-catch structure is used for many situations, but if you want to be thorough in handling the error, you may use the more complete
try-catch-finally structure. The general form of try-catch-finally is
try {
// statement that can throw exceptions
}
catch(ExceptionType obje) {
// error handling statement;
}
finally {
// this code will execute
// with or without encountering
// an error
}
In a try-catch
structure
, if an exception happens, the program control will jump out of the try block, leaving the remaining statements inside the try block unexecuted. If one of those unexecuted statements is critical to the program, for example, closing a file or database connection, that may introduce another problem. This is the kind of situation where you need to use the “finally” clause. The codes written inside a finally clause are guaranteed to execute whether or not an exception happens. Let’s see the file reading code sample again, but this time, with a finally clause.
String filename = "something.txt";
try {
java.io.FileReader reader = new java.io.FileReader(filename);
}
catch(FileNotFoundException e) {
// ask the user to input another filename
}
finally {
// close any connections you may have
// opened e.g. "reader"
}