2019 JavaFundamentals
2019 JavaFundamentals
May not be reproduced in any form without permission from the publisher, except fair uses permitted under U.S. or applicable copyright law.
Copyright
Preface i
Introduction to Java 1
Introduction ..................................................................................................... 2
The Java Ecosystem ........................................................................................ 2
Our First Java Application .............................................................................. 4
Syntax of a Simple Java Program ........................................................................ 4
Exercise 1: A Simple Hello World Program ........................................................ 5
Exercise 2: A Simple Program for Performing Simple
Mathematic Operations ...................................................................................... 6
Exercise 3: Displaying Non-ASCII Characters ..................................................... 7
Activity 1: Printing the Results of Simple Arithmetic Operations ................... 7
Getting Input from the User ................................................................................ 8
Exercise 4: Reading Values from the User and Performing Operations ...... 10
Packages ........................................................................................................ 11
Rules to Follow When Using Packages ............................................................. 12
Activity 2: Reading Values from the User and Performing
Operations Using the Scanner Class ................................................................ 13
Activity 3: Calculating the Percent Increase or Decrease of
Financial Instruments ........................................................................................ 14
Summary ........................................................................................................ 14
Introduction ................................................................................................... 18
Variables and Data Types ............................................................................ 18
Variables .............................................................................................................. 19
Introduction ................................................................................................... 36
Conditional Statements ............................................................................... 36
The if Statement .................................................................................................. 37
The else Statement ............................................................................................. 38
Exercise 6: Implementing a Simple if-else Statement .................................... 38
The else-if Statement ......................................................................................... 39
Exercise 7: Implementing the else-if Statements ........................................... 40
Nested if Statements .......................................................................................... 42
switch case Statements ..................................................................................... 42
Activity 6: Controlling the Flow of Execution Using Conditionals ................. 44
Activity 7: Developing a Temperature System ................................................ 45
Introduction ................................................................................................... 66
Object-Oriented Principles .......................................................................... 66
Classes and Objects ...................................................................................... 67
Object-Oriented Programming ........................................................................ 67
Naming Conventions for Class Names ............................................................. 69
Exercise 11: Working with Classes and Objects .............................................. 70
Exercise 12: Using the Person Class .................................................................. 72
Constructors .................................................................................................. 73
The this Keyword .......................................................................................... 75
Activity 12: Creating a Simple Class in Java ..................................................... 76
Activity 13: Writing a Calculator Class .............................................................. 77
Inheritance .................................................................................................... 78
Types of Inheritance ........................................................................................... 78
Importance of Inheritance in OOP ................................................................... 79
Appendix 303
Index 373
About
This section briefly introduces the author, the coverage of this book, the technical skills you'll
need to get started, and the hardware and software requirements required to complete all of
the included activities and exercises.
Objectives
• Create and run Java programs
• Use data types, data structures, and control flow in your code
• Implement best practices when creating objects
• Work with constructors and inheritance
• Understand advanced data structures to organize and store data
• Employ generics for stronger check-types during compilation
• Learn to handle exceptions in your code
Audience
Java Fundamentals is designed for tech enthusiasts who are familiar with some
programming languages and want a quick introduction to the most important principles
of Java.
Approach
Java Fundamentals takes a practical approach to equip beginners with the most
essential data analysis tools in the shortest possible time. It contains multiple activities
that use real-life business scenarios for you to practice and apply your new skills in a
highly relevant context.
Hardware Requirements
For the optimal student experience, we recommend the following hardware
configuration:
• Processor: Intel Core i7 or equivalent
• Memory: 8 GB RAM
• Storage: 35 GB available space
Software Requirements
You'll also need the following software installed in advance:
• Operating system: Windows 7 or above
• Java 8 JDK
• IntelliJ IDEA
2. Open the downloaded file. You will see the following window. Click Next:
Conventions
Code words in text, database table names, folder names, filenames, file extensions,
pathnames, dummy URLs, user input, and Twitter handles are shown as follows: "The
correct instruction should be System.out.println."
A block of code is set as follows:
public class Test { //line 1
public static void main(String[] args) { //line 2
System.out.println("Test"); //line 3
} //line 4
} //line 5
New terms and important words are shown in bold. Words that you see on the screen,
for example, in menus or dialog boxes, appear in the text like this: "Right-click the src
folder and select New | Class."
Additional Resources
The code bundle for this book is also hosted on GitHub at: https://github.com/
TrainingByPackt/Java-Fundamentals.
We also have other code bundles from our rich catalog of books and videos available at
https://github.com/PacktPublishing/. Check them out!
Introduction
In this first lesson, we are embarking on our study of Java. If you are coming to Java
from a background of working with another programming language, you probably
know that Java is a language for programming computers. But Java goes beyond just
that. It's more than a very popular and successful language that is virtually present
everywhere, it is a collection of technologies. Besides the language, it encompasses a
very rich ecosystem and it has a vibrant community working on many facets to make
the ecosystem as dynamic as it can be.
Every Java program runs under the control of a JVM. Every time you run a Java
program, an instance of JVM is created. It provides security and isolation for the
Java program that is running. It prevents the running of the code from clashing with
other programs within the system. It works like a non-strict sandbox, making it safe
to serve resources, even in hostile environments such as the internet, but allowing
interoperability with the computer on which it runs. In simpler terms, JVM acts as a
computer inside a computer, which is meant specifically for running Java programs.
Note
It is common for servers to have many JVMs in execution simultaneously.
Up in the hierarchy of stock Java technologies is the JRE. The JRE is a collection of
programs that contains the JVM and also many libraries/class files that are needed for
the execution of programs on the JVM (via the java command). It includes all the base
Java classes (the runtime) as well as the libraries for interaction with the host system
(such as font management, communication with the graphical system, the ability to play
sounds, and plugins for the execution of Java applets in the browser) and utilities (such
as the Nashorn JavaScript interpreter and the keytool cryptographic manipulation tool).
As stated before, the JRE includes the JVM.
At the top layer of stock Java technologies is the JDK. The JDK contains all the programs
that are needed to develop Java programs, and it's most important part is the Java
Compiler (javac). The JDK also includes many auxiliary tools such as a Java disassembler
(javap), a utility to create packages of Java applications (jar), system to generate
documentation from source code (javadoc), among many other utilities. The JDK is a
superset of the JRE, meaning that if you have the JDK, then you also have the JRE (and
the JVM).
But those three parts are not the entirety of Java. The ecosystem of Java includes a very
large participation of the community, which is one of the reasons for the popularity of
the platform.
Note
Research into the most popular Java libraries that are used by the top Java projects
on GitHub (according to research that has been repeated in 2016 and 2017)
showed that JUnit, Mockito, Google's Guava, logging libraries (log4j, sl4j), and all
of Apache Commons (Commons IO, Commons Lang, Commons Math, and so on),
marked their presence, together with libraries to connect to databases, libraries for
data analysis and machine learning, distributed computing, and almost anything
else that you can imagine. In other words, for almost any use that you want to
write programs to, there are high chances of an existing library of tools to help you
with your task.
Besides the numerous libraries that extend the functionality of the stock distributions
of Java, there is a myriad of tools to automate builds (for example, Apache Ant, Apache
Maven, and Gradle), automate tests, distribution and continuous integration/delivery
programs (for example, Jenkins and Apache Continuum), and much, much more.
main() should always be declared as shown in the sample. This is because, if main() is
not a public method, it will not be accessed by the compiler, and the java program will
not run. The reason main() is static is because we do not call it using any object, like you
would for all other regular methods in Java.
Note
We will discuss these the public and static keywords later in this book, in greater
depth.
Comments are used to provide some additional information. The Java compiler ignores
these comments.
Single line comments are denoted by // and multiline comments are denoted by /* */.
Note
The solution for this activity can be found on page 304.
Now, we must dissect the structure of our new program, the one with the public
class ReadInput. You might notice that it has more lines and that it is apparently more
complex, but fret not: every single detail will be revealed (in all its full, glorious depth)
when the time is right. But, for now, a simpler explanation will do, since we don't want
to lose our focus on the principal, which is taking input from the user.
First, on line 1, we use the import keyword, which we have not seen yet. All Java code
is organized in a hierarchical fashion, with many packages (we will discuss packages in
more detail later, including how to make your own).
Here, hierarchy means "organized like a tree", similar to a family tree. In line 1 of the
program, the word import simply means that we will use methods or classes that are
organized in the java.io.Exception package.
On line 2, we, as before, create a new public class called ReadInput, without any
surprises. As expected, the source code of this program will have to be inside a source
file called ReadInput.java.
On line 3, we start the definition of our main method, but, this time, add a few words
after the closing parentheses. The new words are throws IOException. Why is this
needed?
The short explanation is: "Because, otherwise, the program will not compile." A longer
version of the explanation is "Because when we read the input from the user, there may
be an error and the Java language forces us to tell the compiler about some errors that
our program may encounter during execution."
Also, line 3 is the line that's responsible for the need of the import in line 1: the
IOException is a special class that is under the java.io.Exception hierarchy.
Line 5 is where the real action begins: we define a variable called inByte (short for "byte
that will be input"), which will contain the results of the System.in.read method.
The System.in.read method, when executed, will take the first byte (and only one) from
the standard input (usually, the keyboard, as we already discussed) and give it back as
the answer to those who executed it (in this case, we, in line 5). We store this result in
the inByte variable and continue the execution of the program.
With line 6, we print (to the standard output) a message saying what byte we read, using
the standard way of calling the System.out.println method.
Notice that, for the sake of printing the byte (and not the internal number that
represents the character for the computer), we had to use a construct of the following
form:
• An open parenthesis
• The word char
• A closing parenthesis
We use this before the variable named inByte. This construct is called a type cast and
will be explained in much more detail in the lessons that follow.
On line 7, we use a different way to print the same message to the standard output. This
is meant to show you how many tasks may be accomplished in more than one way and
that there is "no single correct" way. Here, we use the System.out.println function.
The remaining lines simply close the braces of the main method definition and that of
the ReadInput class.
Some of the main format strings for System.out.printf are listed in the following table:
There are many other formatting strings and many variables, and you can find the full
specification on Oracle's website.
We will see some other common (modified) formatted strings, such as %.2f (which
instructs the function to print a floating-point number with exactly two decimal digits
after the decimal point, such as 2.57 or -123.45) and %03d (which instructs the function
to print an integer with at least three places possibly left filled with 0s, such as 001 or
123 or 27204).
Packages
Packages are namespaces in Java that can be used to avoid name collisions when you
have more than one class with the same name.
For example, we might have more than one class named Student being developed by
Sam and another class with the same name being developed by David. We need a way
to differentiate between the two classes if we need to use them in our code. We use
packages to put the two classes in two different namespaces.
For example, we might have the two classes in two packages:
• sam.Student
• david.Student
Figure 1.3: Screenshot of the sam.Student and david.Student packages in File Explorer
All the classes that are fundamental to the Java language belong to the java.lang
package. All the classes that contain utility classes in Java, such as collections, classes
for localization, and time utilities, belong to the java.util package.
As a programmer, you can create and use your own packages.
To use a class from a package in your code, you need to import the class at the top of
your Java file. For example, to use the Student class, you would import it as follows:
import com.example.Student;
public class MyClass {
Scanner is a useful class in the java.util package. It is an easy way of inputting types,
such as int or strings. As we saw in an earlier exercise, the packages use nextInt() to
input an integer with the following syntax:
sc = new Scanner(System.in);
int x = sc.nextIn()
Activity 2: Reading Values from the User and Performing Operations Using
the Scanner Class
To read two numbers from the user and print their sum, perform the following steps:
1. Create a new class and enter ReadScanner as the class name
2. Import the java.util.Scanner package
3. In the main() use System.out.print to ask the user to enter two numbers of
variables a and b.
4. Use System.out.println to output the sum of the two numbers.
5. Run the main program.
The output should be similar to this:
Enter a number: 12
Enter 2nd number: 23
The sum is 35.
Note
The solution for this activity can be found on page 304.
Note
The solution for this activity can be found on page 305.
Summary
This lesson covered the very basics of Java. We saw some of the basic features of a Java
program, and how we can display or print messages to the console. We also saw how we
can read values using the input consoles. We also looked at packages that can be used
to group classes, and saw an example of Scanner in java.util package.
In the next lesson, we will cover more about how values are stored, and the different
values that we can use in a Java program.
Learning Objectives
Introduction
In the previous lesson, we were introduced to the Java ecosystem and the tools that are
needed to develop Java programs. In this lesson, we will start our journey of the Java
language by looking at the fundamental concepts in the language such as variables, data
types, and operations.
Primitive types are the fundamental types, that is, they cannot be modified. They are
indivisible and form the basis for forming complex types. There are eight primitive data
types in Java, which we will cover in depth in the subsequent sections:
• byte
• short
• int
• long
• char
• float
• double
• boolean
Reference types are types that refer to data that's stored in a certain memory location.
They don't hold the data themselves, but hold the address of the data. Objects, which
will be covered later, are examples of reference types:
For example, an integer can have a value such as 100, support operations such as
addition and subtraction, and is represented using 32-bits on the computer's memory.
Variables
Whenever we want to deal with a given data type, we have to create a variable of that
data type. For example, to create an integer that holds your age, you would use a line
like the following:
int age;
Here, we are saying the variable is called age and is an integer. Integers can only hold
values in the range -2,147,483,648 to 2,147,483,647. Trying to hold a value outside the
range will result in an error. We can then assign a value to the age variable, as follows:
age = 30;
The age variable now holds the value 30. The word age is called an identifier and is used
to refer to the memory location where the value 30 is stored. An identifier is a human-
readable word that is used to refer to the memory address of the value.
You can use a word of your choice as an identifier to refer to the same memory address.
For example, we could have written this as follows:
int myAge ;
myAge = 30;
Here is a graphical representation of the preceding code snippet:
As much as we can use any word as an identifier, Java has some rules on what makes
up a valid identifier. The following are some of the rules to adhere to when creating
identifier names:
• Identifiers should start with either a letter, _, or $. They cannot start with a
number.
• Identifiers can only contain valid unicode characters and numbers.
• Identifiers cannot have spaces in between them.
• Identifiers can be of any length.
• Identifiers cannot be reserved keywords.
• Identifiers cannot have arithmetic symbols such as + or -.
• Identifiers are case-sensitive, for example, age and Age are not the same
identifiers.
Reserved Keywords
Java also contains inbuilt words that are reserved and cannot be used as identifiers.
These words have special meanings in the language.
Now let's discuss the primitive data types in Java. As we said before, Java has 8 primitive
data types, which we will look at in detail.
The num variable is now an int with a value of five. We can also declare more than one
variable of the same type in one line:
int num1, num2, num3, num4, num5;
Here, we have created five variables, all of the int type, and initialized to zero. We can
also initialize all of the variables to a specific value, as follows:
int num1 = 1, num2 = 2, num3 = 3, num4 = 4, num5 = 5;
• To express in binary format, we start the int with 0b or 0B, that is, a zero followed
by b or B. The case doesn't matter. For example, to hold the value 100 in binary, we
would do the following:
int bin_num = 0b1100100;
• To hold the number 999 in binary, we would do the following:
int bin_num1 = 0B1111100111;
As a summary of the aforementioned four formats of representing integers, all the
following variables hold the same value of 117:
int num = 117;
int hex_num = 0x75;
int oct_num = 0165;
int bin_num = 0b1110101;
Since integers are 32-bit and hence lie within the range of long, we can convert an int
into a long.
Type Casting
To convert an int of value of 23 into a long literal, we would need to do what is called
type casting:
int num_int = 23;
long num_long = (long)num_int;
In the second line, we cast the num_int of the int type to a long literal by using the
notation (long)num_int. This is referred to as casting. Casting is the process of
converting one data type into another. Although we can cast a long to an int, remember
that the number might be outside the int range and some numbers will be truncated if
they can't fit into the int.
As is with int, long can also be in octal, hexadecimal, and binary, as shown in the
following code:
long num = 117L;
long hex_num = 0x75L;
long oct_num = 0165L;
long bin_num = 0b1110101L;
{
static Scanner sc = new Scanner(System.in);
public static void main(String[] args)
2. Input a number as an integer:
{
System.out.println("Enter a Number: ");
int num1 = sc.nextInt();
3. Print out the integer:
System.out.println("Entered value is: " + num1);
4. Convert the integer into a floating point:
float fl1 = num1;
5. Print out the floating point:
System.out.print("Entered value as a floating point variable is: " + fl1);
You can assign a byte to a short because all the values of a byte fall in the short's range.
However, the reverse will throw an error, as explained with byte and int. To convert
an int into a short, you have to cast to avoid the compile errors. This also applies to
converting a long into a short:
short num = 13000;
byte num_byte = 19;
num = num_byte; //OK
int num1 = 10;
short s = num1; //Error
long num_long = 200L;
s = (short)num_long; //OK
Note
Some languages, such as like C and C++, allow Booleans to take a value of 1 for
true and 0 for a false. Java doesn't allow you to assign 1 or 0 to Boolean and this
will raise a compile-time error.
Note that chars are enclosed in single quotes, NOT double quotes. Enclosing a char in
double quotes changes it to a string. A string is a collection of one or more chars. An
example of a String is "Hello World":
String hello = "Hello World";
Enclosing a char in double quotes will raise an error because the compiler interprets
double quotes as a string, not a char:
char hello = "Hello World"; //ERROR
Likewise, enclosing more than one character in single quotes raises a compiler error
because chars should be only one character:
String hello = 'Hello World'; //ERROR
In addition to chars being used to hold single characters, they can also be used to hold
escape characters. Escape characters are special characters that have a special use.
They consist of a backslash followed by a character and are enclosed in single quotes.
There are 8 predefined escape characters, as shown in the following table, along with
their uses:
The char holds a newline and if you try printing it to the console, it skips to the next
line.
If you print '\t', a tab is escaped in the output:
char tb = '\t';
Floating types are represented using a special standard referred to as the IEEE 754
Floating-point standard. This standard was set up by the Institute of Electrical and
Electronic Engineers (IEEE) and is meant to make the representation of floating types
uniform in the low level parts of the compute. Remember that floating types are usually
approximations. When we say 5.01, this number has to be represented in binary format
and the representation is usually an approximation to the real number. When working
with very high-performance programs where values have to be measured to the order
of micro numbers, it becomes imperative that you understand how floating types are
represented at the hardware levels to avoid precision loss.
Floating types have two representations: decimal format and scientific notation.
The decimal format is the normal format we usually use, such as 5.4, 0.0004, or
23,423.67.
The scientific notation is the use of the letter e or E to represent a ten raised to a value.
For example, 0.0004 in scientific notation is 4E-4 or 4e-4, which is similar to 4 x 10-4 .
The number 23,423.67 in scientific notation would be 2.342367E4 or 2.342367e4, which
is similar to 2.342367 x 104.
The Float class also has the constant NaN to indicate a number that is not of a float type:
float nan = Float.NaN;
As with the integral types we have discussed, we can assign an int, byte, short, long,
and char to a float, but cannot do the reverse unless we cast.
Note
Casting an integer to a float and then back to an int will not always lead to an
original number. Be careful when doing casting between int and float.
As you might have already guessed it, Java also provides a class called Double with some
useful constants, as shown in the following code:
double max = Double.MAX_VALUE;
double min = Double.MIN_NORMAL;
double max_inf = Double.POSITIVE_INFINITY;
double min_inf = Double.NEGATIVE_INFINITY;
double nan = Double.NaN;
Likewise, we can assign the integral types and float except the boolean type to double
and not the other way round until we cast. The following are example operations that
are allowed and some that are forbidden:
int num = 100;
double d1 = num;
float f1 = 0.34f;
double d2 = f1;
Note
The solution for this activity can be found on page 306.
Note
The solution for this activity can be found on page 307.
Summary
In this lesson, we learned about the use of primitive and reference data types, along
with simple arithmetic operations on data in Java. We learned how to cast data types
from one type to another. We then saw how we can work with floating-point data types.
In the next lesson, we will work with conditional statements and looping structures.
• Control the flow of execution using the if and else statements in Java
• Check through multiple conditions using the switch case statements in Java
• Utilize the looping constructs in Java to write concise code to perform repetitive actions
Introduction
So far, we have looked at programs that consist of a series of statements that the Java
compiler executes sequentially. However, in certain cases, we might need to perform
actions based on the current state of the program.
Consider the example of the software that's installed in an ATM machine – it performs a
set of actions, that is, it allows a transaction to occur when the PIN that's been entered
by the user is correct. However, when the PIN that's been entered is incorrect, then the
software performs another set of actions, that is, it informs the user that the PIN does
not match and asks the user to reenter the PIN. You'll find that such logical constructs
that depend upon values or stages are present in almost all real-world programs.
There are also times where a particular task might need to be performed repeatedly,
that is, for a particular time duration, for a particular set number of times, or until a
condition is met. Continuing from our example of the ATM machine, if the number of
times an incorrect password is entered exceeds three, then the card is blocked.
These logical constructs act as building blocks, as we move toward building complex
programs in Java. This lesson will dive into these basic constructs, which can be
categorized into two general classes, as follows:
• Conditional statements
• Looping statements
Conditional Statements
Conditional statements are used to control the flow of execution of the Java compiler
based on certain conditions. This implies that we are making a choice based on a
certain value or the state of a program. The conditional statements that are available in
Java are as follows:
• The if statement
• The if-else statement
• The else-if statement
• The switch statement
The if Statement
The if statement tests a condition, and when the condition is true, the code contained
in the if block is executed. If the condition is not true, then the code in the block is
skipped and the execution continues from the line after the block.
The syntax for an if statement is as follows:
if (condition) {
//actions to be performed when the condition is true
}
Consider the following example:
int a = 9;
if (a < 10){
System.out.println("a is less than 10");
}
Table 3.1: Table showing the distance and its corresponding fee
}
}
4. Within the main method, create two integer variables, one called distance and
another called fee. The two variables will hold the distance and delivery fees,
respectively. Initialize the distance to 10 and the fee to zero:
int distance = 10;
int fee = 0;
Nested if Statements
We can have if statements inside other if statements. This construct is called a nested
if statement. We evaluate the outer condition first and if it succeeds, we then evaluate
a second inner if statement and so on until all the if statements have finished:
if (age > 20){
We can nest as many statements as we wish to, and the compiler will evaluate them,
starting from the top going downward.
However, with the same logic, when implemented using a switch case statement, it
would look as follows:
switch (age){
case 10:
discount = 300;
case 20:
discount = 200;
case 30:
discount = 100;
default:
discount = 50;
}
Most of the time, what we really wish for is the execution to end at the matched
case. We want it to be so that if the first case is matched, then the code in that case
is executed and the rest of the cases are ignored. To achieve this, we use a break
statement to tell the compiler to continue to execute outside the switch statement.
Here is the same switch case with break statements:
switch (age){
case 10:
discount = 300;
break;
case 20:
discount = 200;
break;
case 30:
discount = 100;
break;
default:
discount = 50;
}
Because the default is the last case, we can safely ignore the break statement because
the execution will end there anyway.
Note:
It is good design to always add a break statement in case another programmer
adds extra cases in the future.
Create a program that calculates and displays the salary earned by the worker based on
the number of hours worked.
To meet this requirement, perform the following steps:
1. Initialize two variables and the values of the working hours and salary.
2. In the if condition, check whether the working hours of the worker is below the
required hours. If the condition holds true, then the salary should be (working
hours * 10).
3. Use the else if statement to check if the working hours lies between 8 hours and
12 hours. If that is true, then the salary should be calculated at $10 per hour for the
first eight hours and the remaining hours should be calculated at $12 per hour.
4. Use the else block for the default of $160 (additional day's salary) per day.
5. Execute the program to observe the output.
Note
The solution for this activity can be found on page 308.
6. After you complete the switch construct, print the value of weatherWarning.
7. Run the program to see the output, it should be similar to:
Its cold outside, do not forget your coat.
Note
The solution for this activity can be found on page 309.
Looping Constructs
Looping constructs are used to perform a certain operation a given number of times
as long as a condition is being met. They are commonly used to perform a specific
operation on the items of a list. An example is when we want to find the summation of
all the numbers from 1 to 100. Java supports the following looping constructs:
• for loops
• for each loops
• while loops
• do while loops
for Loops
The syntax of the for loop is as follows:
for( initialization ; condition ; expression) {
//statements
}
The initialization statements are executed when the for loop starts executing. It can be
more than one expression, all separated by commas. The expressions must all be of the
same type:
for( int i = 0, j = 0; i <= 9; i++)
The condition section of the for loop must evaluate to true or false. If there is no
expression, the condition defaults to true.
The expression part is executed after each iteration of the statements, as long as the
condition is true. You can have more than one expression separated by a comma.
Note
The expressions must be valid Java expressions, that is, expressions that can be
terminated by a semicolon.
4. Implement a for loop that initializes a variable i at zero, a condition so that the
value remains below 10, and i should be incremented by one in each iteration:
System.out.println("Increasing order");
for( int i = 0; i <= 9; i++)
System.out.println(i);
5. Implement another for loop that initializes a variable k at 9, a condition so that the
value remains above 0, and k should be decremented by one in each iteration:
System.out.println("Decreasing order");
for( int k = 9; k >= 0; k--)
System.out.println(k);
Output:
Increasing order
0
1
2
3
4
5
6
7
8
9
Decreasing order
9
8
7
6
5
4
3
2
1
0
Note
The solution for this activity can be found on page 310.
All three sections of the for loop are optional. This implies that the line for( ; ;) will
provide any error. It just provides an invite loop.
This for loop doesn't do anything and won't terminate. Variables declared in the for
loop declaration are available in the statements of the for loop. For example, in our first
example, we printed the value of i from the statements sections because the variable i
was declared in the for loop. This variable is, however, not available after the for loop
and can be freely declared. It can't however be declared inside the for loop again:
for (int i = 0; i <= 9; i++)
int i = 10; //Error, i is already declared
For loops can also have braces enclosing the statements if we have more than one
statement. This is just as we discussed in the if-else statements earlier. If we have only
one statement, then we don't need to have braces. When the statements are more than
one, they need to be enclosed within braces. In the following example, we are printing
out the value of i and j:
for (int i = 0, j = 0; i <= 9; i++, j++) {
System.out.println(i);
System.out.println(j);
}
Note
The expressions must be valid Java expressions, that is, expressions that can be
terminated by a semicolon.
A break statement can be used to interrupt the for loop and break out of the loop. It
takes the execution outside the for loop.
For example, we might wish to terminate the for loop we created earlier if i is equal to
5:
for (int i = 0; i <= 9; i++){
if (i == 5)
break;
System.out.println(i);
}
Output:
0
1
2
3
4
The preceding for loop iterates from 0, 1, 2, and 3 and terminates at 4. This is because
after the condition i, that is, 5 is met, the break statement is executed, which ends the
for loop and the statements after it are not executed. Execution continues outside the
loop.
The continue statement is used to tell the loop to skip all the other statements after it
and continue execution to the next iteration:
for (int i = 0; i <= 9; i++){
if (i == 5)
continue;
System.out.println(i);
}
Output:
0
1
2
3
4
6
7
8
9
The number 5 is not printed because once the continue statement is encountered,
the rest of the statements after it are ignored, and the next iteration is started. The
continue statements can be useful when there are a few exceptions you wish to skip
when processing multiple items.
}
}
Output:
11 12 13
21 22 23
31 32 33
For each single loop of i, we loop j three times. You can think of these for loops as
follows:
Repeat i three times and for each repetition, repeat j three times. That way, we have a
total of 9 iterations of j. For each iteration of j, we then print out the value of i and j.
}
}
}
}
4. Within this loop, create two more for loops, one to print the spaces and the other
to print the *:
for (int k = 0; k < (7 - i / 2); k++) {
System.out.print(" ");
}
for (int j = 1; j <= i; j++) {
System.out.print("*");
}
5. Within the outer for loop, add the following code to add the next line:
System.out.println();
Run the program. You will see the resultant pyramid.
for-each Loops
for each loops are an advanced version of for loops that were introduced in Java 5.
They are used to perform a given operation on every item in an array or list of items.
Let's take a look at this for loop:
int[] arr = { 1, 2, 3, 4, 5 , 6, 7, 8, 9,10};
for (int i = 0; i < 10; i++){
System.out.println(arr[i]);
}
The first line declares an array of integers. An array is a collection of items of the same
type. In this case, the variable arr is holding a collection of 10 integers. We then use a
for loop from 0 to 10, printing the elements of this array. We are using i < 10 because
the last item is at index 9, not 10. This is because the elements of an array start with
index 0. The first element is at index 0, the second at index 1, the third at 2, and so on.
arr[0] will return the first element, arr[1] the second, arr[2] the third, and so on.
This for loop can be replaced with a shorter for each loop. The syntax of a for each
loop is as follows:
for( type item : array_or_collection){
//Code to executed for each item in the array or collection
}
For our preceding example, the for each loop would be as follows:
for(int item : arr){
System.out.println(item);
}
int item is the current element in the array we are at. The for each loop will iterate
for all the elements in the array. Inside the braces, we print out the item. Note that we
didn't have to use arr[i] like in the for loop earlier. This is because the for each loop
automatically extracts the value for us. In addition, we didn't have to use an extra int i
to keep the current index and check if we are below 10 (i < 10), like in the for loop we
used earlier. for each loops are shorter and automatically check the range for us.
For example, we can use the for each loop to print the squares of all the elements
present in the array, arr:
for(int item : arr){
int square = item * item;
System.out.println(square);
}
Output:
1
4
9
16
25
36
49
64
81
10
For example, to print all of the numbers from 0 to 10 using a while loop, we would use
the following code:
public class Loops {
public static void main(String[] args){
int number = 0;
while (number <= 10){
System.out.println(number);
number++;
}
}
}
Output:
0
1
2
3
4
5
6
7
8
9
10
We could also write the preceding code using a do while loop:
public class Loops {
public static void main(String[] args){
int number = 0;
do {
System.out.println(number);
number++;
}while (number <= 10);
}
}
With the do while loop, the condition is evaluated last, so we are sure that the
statements will be executed at least once.
Note
The solution for this activity can be found on page 311.
Note
The solution for this activity can be found on page 312.
We get the incoming number of peaches from John and add it to the current number
of peaches. Then, we print a message for each group of 20 peaches and say how many
boxes we have shipped and how many peaches we have left, e.g., "2 boxes shipped, 54
peaches remaining". We would like to do this with a while loop. The loop will continue
as we have a number of peaches that would fit at least one box. We will have another
while loop that gets the next batch and quits if there is none. To achieve this, perform
the following steps:
1. Create a new class and enter PeachBoxCount as the class name
2. Import the java.util.Scanner package:
3. Create a numberOfBoxesShipped variable and a numberOfPeaches variable.
4. In the main(), write an infinite while loop.
5. Use System.out.print to ask the user for the incomingNumberOfPeaches. If this is
zero, break out of this infinite loop.
6. Add the incoming peaches to the existing peaches.
7. Write a while loop that continues as we have at least 20 peaches.
8. In the for loop, remove 20 peaches from numberOfPeaches and increment
numberOfBoxesShipped by 1. Print these values.
9. Run the main program.
The output should be similar to:
Enter the number of peaches picked: 23
1 boxes shipped, 3 peaches remaining
Enter the number of peaches picked: 59
2 boxes shipped, 42 peaches remaining
3 boxes shipped, 22 peaches remaining
4 boxes shipped, 2 peaches remaining
Enter the number of peaches picked: 0
Note
The solution for this activity can be found on page 313.
Summary
In this lesson, we've covered some of the fundamental and important concepts in Java
and programming by looking at some simple examples. Conditional statements and
looping statements are normally essential to implementing logic.
In the next lesson, we will focus on a couple more fundamental concepts, such as
functions, arrays, and strings. These concepts will help us in writing concise and
reusable code.
Learning Objectives
Introduction
So far, we've looked at the basics of Java and how to use simple constructs such as
conditional statements and looping statements, and how methods are implemented in
Java. These basic ideas are very important to understand and are useful when building
simple programs. However, to build and maintain large and complex programs, the
basic types and constructs do not suffice. What makes Java really powerful is the fact
that it is an object-oriented programming language. It allows you to build and integrate
complex programs effectively, while maintaining a consistent structure, making it easy
to scale, maintain, and reuse.
In this lesson, we will introduce a programming paradigm called object-oriented
programming (OOP), which lies at the core of Java. We will have a look at how OOP is
done in Java and how you can implement it to design better programs.
We will start this lesson with a definition of OOP and the principles underlying it, will
look at OOP constructs called classes and objects, and will conclude the lesson by
looking at a concept called inheritance.
We will write two simple OOP applications in Java: one to represent people who are
normally found in a university, such as students, lecturers, and the staff, and the other
to represent domestic animals in a farm. Let's get started!
Object-Oriented Principles
OOP is governed by four main principles, as follows. Throughout the rest of this lesson,
we will delve further into each of these principles:
• Inheritance: We will learn how we can reuse code by using hierarchies of classes
and inheriting behavior from derived classes
• Encapsulation: We will also look at how we can hide the implementation details
from the outside world while providing a consistent interface to communicate
with our objects through methods
• Abstraction: We will look at how we can focus on the important details of an
object and ignore the other details
• Polymorphism: We will also have a look at how we can define abstract behaviors
and let other classes provide implementations for these behaviors
Object-Oriented Programming
Object-oriented programming, often referred to as OOP, is a style of programming in
which we deal with objects. Objects are entities that have properties to hold their data
and methods to manipulate the data.
Let's break this down into simpler terms.
In OOP, we primarily deal with objects and classes. An object is a representation
of a real-world item. An example of an object is your car or yourself. An object has
properties associated with it and actions it can perform. For example, your car has
wheels, doors, an engine, and gears, which are all properties, and it can perform
actions such as speeding, braking, and stopping, which are all called methods. The
following diagram is an illustration of the properties and methods you have, as a person.
Properties can sometimes be referred to as fields:
In OOP, we define classes as blueprints of our items and objects as instances of classes.
In the preceding diagram, the Person class is used to represent all people, regardless of
their gender, age, or height. From this class, we can create specific examples of people,
as shown in the boxes inside the Person class.
In Java, we mainly deal with classes and objects, so it is very important that you
understand the difference between the two.
Note
In Java, everything except primitive data types are objects.
}
The modifier is public, meaning that the class can be accessed from other Java
packages. The class name is Person.
Here is a more robust example of the Person class with a few properties and methods:
public class Person {
//Properties
int age;
int height;
String name;
//Methods
These properties are used to hold the state of the object. That is, age holds the age of
the current person, which can be different from that of the next person. name is used to
hold the name of the current person, which will also be different from the next person.
They answer the question: who is this person?
The methods are used to hold the logic of the class. That is, they answer the question:
what can this person do? Methods can be private, public, or protected.
The operations in the methods can be as complex as your application needs. You can
even call methods from other methods, as well as adding parameters to those methods.
//Properties
int age;
int height;
String name;
3. Define three methods, that is, walk(), sleep(), and takeShower(). Write the print
statements for each so that you can print out the text to the console when they
are called:
//Methods
public void walk(){
//Do walking operations here
System.out.println("Walking...");
}
public void sleep(){
//Do sleeping operations here
System.out.println("Sleeping...");
}
private void takeShower(){
//Do take shower operations here
System.out.println("Taking a shower...");
}
4. Now, pass the speed parameter to the walk() method. If the speed is above 10, we
print the output to the console, otherwise we don't:
public void walk(int speed){
//Do walking operations here
if (speed > 10)
{
System.out.println("Walking...");
}
5. Now that we have the Person class, we can create objects for it using the new
keyword. In the following code, we have created three objects:
Person me = new Person();
Person myNeighbour = new Person();
Person lecturer = new Person();
The me variable is now an object of the Person class. It represents a specific type of
person, me.
With this object, we can do anything we wish, such as calling the walk() method, calling
the sleep() method, and much more. We can do this as long as there are methods in the
class. Later, we will look at how we can add all of this behavior to a class. This code will
not have any output since we do not have the main method.
lecturer.walk(20);
lecturer.walk(5);
lecturer.sleep();
}
7. Run the program again and observe the output:
Walking...
Sleeping...
Walking...
Sleeping...
Walking...
Sleeping...
In this example, we created a new class called PersonTest and inside it created three
objects of the Person class. We then called the methods of the me object. From this
program, it is evident that the Person class is a blueprint from which we can create
as many objects as we wish. We can manipulate each of these objects separately as
they are completely different and independent. We can pass these objects around as
if they were just like any other variables, and can even pass them to other objects as
parameters. This is the flexibility of object-oriented programming.
Note
We didn't call me.takeShower() because this method is declared private in the
Person class. Private methods cannot be called outside their class.
Constructors
To be able to create an object of a class, we need a constructor. A constructor is
called when you want to create an object of a class. When we create a class without a
constructor, Java creates an empty default constructor for us that takes no parameters.
If a class is created without a constructor, we can still instantiate it with the default
constructor. A good example of this is the Person class that we used previously. When
we wanted a new object of the Person class, we wrote the following:
Person me = new Person();
The default constructor is Person(), and it returns a new instance of the Person class.
We then assign this returned instance to our variable, me.
A constructor is just like any other method, except for a few differences:
• A constructor has the same name as the class
• A constructor can be public or private
• A constructor doesn't return anything, even void
Let's look at an example. Let's create a simple constructor for our Person class:
public class Person {
//Properties
int age;
int height;
String name;
//Constructor
public Person(int myAge){
age = myAge;
}
//Methods
public void walk(int speed){
//Do walking operations here
if (speed > 10)
System.out.println("Walking...");
}
public void sleep(){
//Do sleeping operations here
System.out.println("Sleeping...");
}
private void takeShower(){
//Do take shower operations here
System.out.println("Taking a shower...");
}
}
This constructor takes one argument, an integer called myAge, and assigns its value
to the age property in the class. Remember that the constructor implicitly returns an
instance of the class.
We can use the constructor to create the me object again, this time passing age:
Person me = new Person(30);
In this line, as we saw earlier, we are setting the age variable in our current object to the
new value, myAge, which is passed in as a parameter. Sometimes, we wish to be explicit
about the object we are referring to. When we want to refer to the properties in the
current object we are dealing with, we use the this keyword. As an example, we could
rewrite the preceding line as follows:
this.age = myAge;
In this new line, this.age is used to refer to the age property in the current object we
are dealing with. this is used to access the current object's instance variables.
For example, in the preceding line, we are setting the current object's age to the value
that's passed into the constructor.
In addition to referring to the current object, this can also be used to invoke a class'
other constructors if you have more than one constructor.
In our Person class, we will create a second constructor that takes no parameter. If this
constructor is invoked, it invokes the other constructor we created with a default value
of 28:
//Constructor
public Person(int myAge){
this.age = myAge;
}
public Person(){
this(28);
}
Now, when the call of Person me = new Person() is made, the second constructor will
call the first constructor with myAge set to 28. The first constructor will then set the
current object's age to 28.
Note
The solution for this activity can be found on page 314.
Note
The solution for this activity can be found on page 318.
Inheritance
In this section, we will have a look at another important principle of OOP, called
inheritance. Inheritance in OOP has the same meaning as it has in English. Let's look at
an example by using our family trees. Our parents inherit from our grandparents. We
then inherit from our parents, and finally, our children inherit, or will inherit, from us.
Similarly, a class can inherit the properties of another class. These properties include
methods and fields. Then, another class can still inherit from it, and so on. This forms
what we call an inheritance hierarchy.
The class being inherited from is called the superclass or the base class, and the class
that is inheriting is called the subclass or the derived class. In Java, a class can only
inherit from one superclass.
Types of Inheritance
An example of inheritance is a management hierarchy in a company or in the
government:
• Single Level Inheritance: In single level inheritance, a class inherits from only one
other class:
• Multiple inheritance: Here, a class can inherit from more than one class:
Multiple inheritance is not directly supported in Java, but can be achieved by using
interfaces, which will be covered in the next lesson.
}
We use the extends keyword to denote inheritance.
For example, if we wanted our Student class to extend the Person class, we would
declare it like so:
public class Student extends Person {
In this Student class, we have access to the public properties and methods that we
defined earlier in the Person class. When we create an instance of this Student class, we
automatically have access to the methods we defined in the Person class earlier, such as
walk() and sleep(). We don't need to recreate those methods anymore as our Student
class is now a subclass of the Person class. We, however, don't have access to private
methods such as takeShower().
Note
Please note that a subclass only has access to the public properties and methods
in its superclass. If a property or method is declared as private in the superclass,
we cannot access it from the subclass. By default, the properties we declared are
only accessible from classes in the same package, unless we specifically put the
public modifier before them.
In our Person class, let's define some common properties and methods that all people
have. Then, we will inherit these properties from this class to create other classes, such
as Student and Lecturer:
public class Person {
//Properties
int age;
int height;
int weight;
String name;
//Constructors
public Person(int myAge, int myHeight, int myWeight){
this.age = myAge;
this.height = myHeight;
this.weight = myWeight;
}
public Person(){
this(28, 10, 60);
}
//Methods
public void walk(int speed){
if (speed > 10)
System.out.println("Walking...");
}
public void sleep(){
System.out.println("Sleeping...");
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
public int getHeight(){
return height;
}
public int getWeight(){
return weight;
}
}
Here, we have defined four properties, two constructors, and seven methods. Can you
explain what each method does? The methods are fairly simple for now so that we can
focus on the core concepts of inheritance. We have also modified the constructors to
take three parameters.
Let's create a Student class that inherits from this Person class, create an object of the
class, and set the name of the student:
public class Student extends Person {
public static void main(String[] args){
Student student = new Student();
student.setName("James Gosling");
}
}
We have created a new Student class that inherits from the Person class. We have
also created a new instance of the Student class and set its name. Note that we didn't
redefine the setName() method in the Student class because it is already defined in the
Person class. We can also call other methods on our student object:
public class Student extends Person {
public static void main(String[] args){
Student student = new Student();
student.setName("James Gosling");
student.walk(20);
student.sleep();
System.out.println(student.getName());
System.out.println(student.getAge());
}
}
Note that we did not create these methods in the Student class as they are already
defined in the Person class from which the Student class inherits.
Let's define a Lecturer class that inherits from the same Person class:
public class Lecturer extends Person {
public static void main(String[] args){
Lecturer lecturer = new Lecturer();
lecturer.setName("Prof. James Gosling");
lecturer.walk(20);
lecturer.sleep();
System.out.println(lecturer.getName());
System.out.println(lecturer.getAge());
}
}
Note
Please note how Inheritance has helped us reduce the amount of code we write by
reusing the same Person class. Without inheritance, we would have had to repeat
the same methods and properties in all of our classes.
As the previous calculator, this calculator also has a method operate that returns a
double, but instead of any login in there, it delegates the current operator, determined
in the constructor.
To complete this activity you'll need to:
1. Create a class Operator that has one String field initialized in the constructor
that represents the operator. This class should have a default constructor that
represents the default operator, which is sum. The operator class should also have
a method called operate that receives two doubles and return the result of the
operator as a double. The default operation is sum.
2. Create three other classes: Subtraction, Multiplication and Division. They extend
from Operator and override the operate method with each operation that they
represent. They also need a no-argument constructor that calls super passing the
operator that they represent.
3. Create a new class, called CalculatorWithFixedOperators. This class will contain
four fields that are constants (finals) and represent the four possible operations.
It should also have three other fields: operand1 and operator2 of type double
and operator of type Operator. These other three fields will be initialized in the
constructor that will receive the operands and the operator as a String. Using the
match methods of the possible operators, determine which one will be set as the
operator fields.
4. As the previous Calculator class, this one will also have an operate method, but it
will only delegate to the operator instance.
5. Last, write a main method that calls the new calculator a few times, printing the
results of the operation for each time.
Note
Rewriting the calculator to use more classes seems more complex than the initial
code. But it abstracts some important behavior which opens some possibilities
that will be explored in future activities.
Note
The solution for this activity can be found on page 319.
Overloading
The next principle of OOP we will discuss is called overloading. Overloading is a
powerful concept in OOP that allows us to reuse method names as long as they have
different signatures. A method signature is the method name, its parameters, and the
order of the parameters:
The preceding is an example of a method that withdraws funds from a given bank name.
The method returns a double and accepts a String parameter. The method signature
here is the name of the getMyFundsFromBank() method and the String parameter
bankName. The signature doesn't include the return type of the method, only the name
and the parameters.
With overloading, we are able to define more than one method with the same method
names but different parameters. This can be useful in defining methods that do the
same thing but take different parameters.
Let's look at an example.
Let's define a class called Sum with three overloaded methods that add the parameters
that are passed and returns the result:
public class Sum {
return (x + y + z);
}
//This sum takes two double parameters
public double sum(double x, double y) {
return (x + y);
}
Let's go back to our Student class and create two overloaded methods. In the first
method, we will print a string to print "Going to class...", regardless of which day of the
week it is. In the second method, we will pass the day of the week and check whether it
is the weekend. If it is the weekend, we will print out a different string in comparison to
the rest of the week. Here is how we will implement this:
public class Student extends Person {
//Add this
public void goToClass(){
System.out.println("Going to class...");
}
public void goToClass(int dayOfWeek){
if (dayOfWeek == 6 || dayOfWeek == 7){
System.out.println("It's the weekend! Not to going to class!");
}else {
System.out.println("Going to class...");
}
}
public static void main(String[] args){
//Add this
student.goToClass();
student.goToClass(6);
}
}
Open the Lecturer class we created and add two overloaded methods, as follows:
• teachClass() prints out "Teaching a random class"
• teachClass(String className) prints out "Teaching " + className
We can overload the main method in a class, but once the program starts up, the JVM
will only call main(String[] args). We can call our overloaded main method from this
main method. Here is an example:
public class Student {
public static void main(String[] args){
// Will be called by the JVM
}
public static void main(String[] args, String str1, int num){
//Do some operations
}
public static void main(int num, int num1, String str){
}
}
In this example, the main method is overloaded three times. However, when we run our
program, the main method whose signature is main(String[] args) will be called. From
anywhere in our code, we can then freely call the other main methods.
Constructor Overloading
Just like methods, constructors can be overloaded too. When the same constructors
are declared with different parameters in the same class, this is known as constructor
overloading. The compiler differentiates which constructor is to be called, depending
on the number of parameters and their data types.
In our discussion on constructors, we created a second constructor for our Person
class that takes age, height, and weight as parameters. We can have this constructor in
the same class as the constructor that takes in no parameters. This is because the two
constructors have a different signature and can hence be used side by side. Let's look at
how we can do this:
//Constructors
public Person(){
this(28, 10, 60);
}
//Overloaded constructor
public Person(int myAge, int myHeight, int myWeight){
this.age = myAge;
this.height = myHeight;
this.weight = myWeight;
}
The two constructors have same name (the class name) but take different parameters.
Add a third constructor that takes age, height, weight, and name. Inside the constructor,
set all the class variables to the passed parameters.
The code is as follows:
public Person(int myAge, int myHeight, int myWeight, String name){
this.age = myAge;
this.height = myHeight;
this.weight = myWeight;
this.name = name;
}
• Method overriding means having two methods with the same arguments, but
different implementations. One of them would exist in the parent class, while
another would exist in the child class:
class Parent {
void foo(double d) {
// do something
}
}
Annotations
We will now cover another important topic that will help us write better Java programs.
Annotations are a way in which we can add metadata to our programs. This metadata
can include information such as the version of a class we are developing. This is useful
in scenarios where a class is deprecated or where we are overriding a certain method.
Such metadata is not part of the program itself, but can help us catch errors or offer
guidance. Annotations have no direct effect on the operation of the code they annotate.
Let's look at a scenario. How do we ensure that we are overriding a certain method
and not creating another completely different method? When overriding a method, a
single mistake such as using a different return type will cause the method to not be
overridden anymore. Such a mistake is easy to make but can lead to software bugs
later on if not taken care of early in the software development stages. How, then,
do we enforce overriding? The answer, as you might have already guessed, is using
annotations.
The @ character indicates to the compiler that what follows is an annotation.
This annotation accepts the name of the author and the date. We can then use this
annotation in our Student class:
@Author(name = "James Gosling", date = "1/1/1970")
public class Student extends Person {
}
You can replace the name and date with your values in the preceding example.
References
As you work with objects, it is important that you understand references. A reference is
an address that indicates where an object's variables and methods are stored.
When we assign objects to variables or pass them to methods as parameters, we aren't
actually passing the object itself or its copy – we are passing references to the objects
themselves in memory.
To better understand how references work, let's illustrate this with an example.
Following is an example:
Create a new class called Rectangle, as follows:
public class Rectangle {
int width;
int height;
r1.height = 300;
r1.width = 400;
System.out.println("r1: width= " + r1.width + ", height= " +
r1.height);
System.out.println("r2: width= " + r2.width + ", height= " +
r2.height);
}
}
The output is as follows::
r1: width= 400, height= 300
r2: width= 400, height= 300
Here is a summary of what happens in the preceding program:
1. We create two variables, r1 and r2, of type Rectangle.
2. A new Rectangle object is assigned to r1.
3. The value of r1 is assigned to r2.
4. The width and height of r2 are changed.
5. The values of the two objects are finally printed.
You might have expected the values of r1 and r2 to have different values. However, the
output says otherwise. This is because when we used r2 = r1 , we created a reference
from r2 to r1 instead of creating r2 as a new object copied from r1. That is, r2 points
to the same object that was pointed to by r1. Either variable can be used to refer to the
object and change its variables:
Note
There are no explicit pointers or pointer arithmetic in Java, as there is in C and C++.
By using references, however, most pointer capabilities are duplicated without
many of their drawbacks.
Aim: To understand how to inherit from a class, overload and override methods, and
create annotations in Java.
Procedure:
1. Open up the Animals project we created earlier.
2. In the project, create a new file named Cat.java in the src/ folder.
3. Open Cat.java and inherit from the Animals class.
4. In it, create a new instance of the Cat class and set the family to "Cat", the name to
"Puppy", ears to two, eyes to two, and legs to four. Don't redefine these methods
and fields – instead, use the inherited ones from the Animals class.
5. Print the family, name, ears, legs, and eyes. What is the output?
Note
The solution for this activity can be found on page 322.
Summary
In this lesson, we have learned that classes are blueprints from which we can create
objects, while objects are instances of a class and provide a specific implementation of
that class. A class can be public, private, or protected. A class has a default constructor
that takes no parameters. We can have user-defined constructors in Java. The this
keyword is used to refer to the current instance of a class.
We then learned that inheritance is a property where a subclass inherits the properties
of a superclass.
We went on to study overloading, polymorphism, annotation, and references in Java.
In the next lesson, we will have a look at the use of interfaces and the Object class in
Java.
• Perform typecasting
Introduction
In the previous lesson, we looked at the basics of object-oriented programming, such as
classes and objects, inheritance, polymorphism, and overloading.
We saw how classes act as a blueprint from which we can create objects, and saw how
methods define the behavior of a class while fields hold the state.
We looked at how a class can acquire properties from another class through inheritance
to enable us to reuse code. Then, we learned how we can reuse a method name through
overloading – that is, as long as they have different signatures. Finally, we had a look at
how subclasses can redefine their own unique behavior by overriding methods from the
superclass.
In this lesson, we will delve deeper into the principles of object-oriented programming
and how to better structure our Java programs.
We will start with interfaces, which are constructs that allow us to define a generic
behavior that any class can implement. We will then learn about a concept called
typecasting, whereby we can change a variable from one type to another and back. In
the same manner, we will deal with primitive data types as objects by using wrapper
classes that are provided by Java. We will finish off with a detailed look at abstract
classes and methods, which is a way to let users who are inheriting your class to run
their own unique implementation.
In this lesson, we will walk through three activities by using the Animal class we created
in the previous lesson. We will also be using our Person class to demonstrate some of
these concepts.
Let's get started!
Interfaces
In Java, you can use interfaces to provide a set of methods that classes must implement
for them to be conformant.
Let's take the example of our Person class. We want to define a set of actions that define
the behavior of any person, regardless of their age or gender.
A few examples of these actions include sleeping, breathing, and moving/walking. We
can place all of these common actions in an interface and let any class that claims to be
a person implement them. A class that implements this interface is often referred to as
being of the type Person.
In Java, we use the keyword interface to denote that the following block will be
an interface. All the methods in an interface are empty and are not implemented.
This is because any class that will implement this interface will provide its unique
implementation details. Therefore, an interface is essentially a group of methods with
no bodies.
Let's create an interface to define the behavior of a person:
public interface PersonBehavior {
void breathe();
void sleep();
void walk(int speed);
}
This interface is called PersonBehavior and it contains three methods: one to breathe,
another one to sleep, and one to walk at a given speed. Every class that implements this
interface will have to also implement these three methods.
We use the implements keyword after a class name, followed by the interface name,
when we want to implement a given interface.
Let's see this with an example. We will create a new class called Doctor to represent
doctors. This class will implement the PersonBehavior interface:
public class Doctor implements PersonBehavior {
}
Because we have stated that we want to conform to the PersonBehavior interface, the
compiler will give us an error if we don't implement the three methods in the interface:
public class Doctor implements PersonBehavior {
@Override
public void breathe() {
}
@Override
public void sleep() {
}
@Override
}
We use the @Override annotation to indicate that this method is from the interface.
Inside these methods, we are free to perform any kind of operations that are relevant to
our Doctor class.
In the same spirit, we can also create an Engineer class that implements the same
interface:
public class Engineer implements PersonBehavior {
@Override
public void breathe() {
}
@Override
public void sleep() {
}
@Override
public void walk(int speed) {
}
}
In Lesson 1, Introduction to Java, we mentioned abstraction as one of the underlying
principles of OOP. Abstraction is a way for us to provide a consistent interface to our
classes.
Let's use a mobile phone as an example. With a mobile phone, you are able to call
and text your friends. When calling, you press the call button and immediately get
connected to a friend. That call button forms an interface between you and your friend.
We don't really know what happens when we press the button because all those details
are abstracted (hidden) from us.
You will often hear the term API, which stands for Application Programming Interface.
It is a way for different software to speak to each other in harmony. An example is when
you want to log in to an app using Facebook or Google. The application will call the
Facebook or Google API. The Facebook API will then define the rules to be followed to
log in.
A class in Java can implement more than one interface. These extra interfaces are
separated by a comma. The class must provide implementations for all the methods it
promises to implement in the interfaces:
public class ClassName implements InterfaceA, InterfaceB, InterfaceC {
2. Open our Doctor class and add the PersonListener interface after the
PersonBehavior interface, separated by a comma:
public class Doctor implements PersonBehavior, PersonListener {
3. Implement the two methods in our PersonListener interface. When the doctor
walks, we will perform some actions and raise the onPersonWalking event to let
other listeners know that the doctor is walking. When the doctor sleeps, we shall
raise the onPersonSleeping event. Modify the walk() and sleep() methods to look
like this:
@Override
public void breathe() {
}
@Override
public void sleep() {
//TODO: Do other operations here
// then raise event
this.onPersonSleeping();
}
@Override
public void walk(int speed) {
//TODO: Do other operations here
// then raise event
this.onPersonWalking();
}
@Override
public void onPersonWalking() {
System.out.println("Event: onPersonWalking");
}
@Override
public void onPersonSleeping() {
System.out.println("Event: onPersonSleeping");
}
4. Add the main method to test our code by calling walk() and sleep():
public static void main(String[] args){
Doctor myDoctor = new Doctor();
myDoctor.walk(20);
myDoctor.sleep();
}
5. Run the Doctor class and see the output in the console. You should see something
like this:
myDoctor.walk(20);
myDoctor.sleep();
}
@Override
public void breathe() {
}
@Override
public void sleep() {
//TODO: Do other operations here
// then raise event
this.onPersonSleeping();
}
@Override
public void walk(int speed) {
//TODO: Do other operations here
// then raise event
this.onPersonWalking();
}
@Override
public void onPersonWalking() {
System.out.println("Event: onPersonWalking");
}
@Override
Note
Since a class can implement more than one interface, we can use interfaces in Java
to simulate multiple inheritance.
8. Override the makeSound() such that movementType is "Moo" and the onAnimalMoved()
method is called.
9. Override the onAnimalMoved() and inAnimalMadeSound() methods.
10. Create a main() to test the code.
Note
The solution for this activity can be found on page 323.
Typecasting
We have already seen how, when we write int a = 10, a is of integer data type, which
is usually 32 bits in size. When we write char c = 'a', c has a data type of character.
These data types were referred to as primitive types because they can be used to hold
simple information.
Objects also have types. The type of an object is often the class of that object. For
example, when we create an object such as Doctor myDoctor = new Doctor(), the
myDoctor object is of type Doctor. The myDoctor variable is often referred to as a
reference type. As we discussed earlier, this is because the myDoctor variable doesn't
hold the object itself. Rather, it holds the reference to the object in memory.
Typecasting is a way for us to change the class or interface from one type to another.
It's important to note that only classes or interfaces (together, these are called types)
that belong to the same superclass or implement the same interface, that is, they have a
parent-child relationship, can be cast or converted into each other.
Let's go back to our Person example. We created the Student class, which inherits from
this class. This essentially means that the Student class is in the Person family and so is
any other class that inherits from the Person class:
For downcasting to work, the object must have originally been of the subclass type. For
example, the following operation is not possible:
Student student = new Student();
Person person = (Person)student;
Lecturer lecturer = (Lecturer) person;
If you try to run this program, you will get the following exception:
This is because person was not originally a Lecturer type, but rather a Student type. We
will talk more about exceptions in the upcoming lessons.
To avoid such kinds of exceptions, you can use the instanceof operator to first check
whether an object is of a given type:
if (person instanceof Lecturer) {
Lecturer lecturer() = (Lecturer) person;
}
The instanceof operator returns true if person was originally of type Lecturer, or
returns false otherwise.
With your data source and the new SalesWithCommission, you'll write an application that
will call the EmployeeLoader.getEmployee method a few times using a for loop. With each
generated employee, it will print their net salary and the tax they pay. It will also check
if the employee is an instance of SalesWithCommission, cast it and print his commission.
To complete this activity you'll need to:
1. Create a SalesWithCommission class that extends Sales. Add a constructor that
receives the gross sales as double and store it as a field. Also add a method called
getCommission which returns a double that is the gross sales times 15% (0.15).
2. Create another class that will work as a data source, generating employees. This
class has one method getEmployee() that will create an instance of one of the
implementations of Employee and return it. The method return type should be
Employee.
3. Write an application that calls getEmployee() repeatedly inside a for loop and
print the information about the Employee salary and tax. And if the employee is an
instance of SalesWithCommission, also print his commission.
Note
The solution for this activity can be found on page 325.
We can also skip the new keyword and the compiler will implicitly wrap it for us:
Integer a = 1;
We can then use the object as if it was any other object. We can upcast it to Object and
then downcast it back to an Integer.
This operation of converting a primitive type into an object (reference type) is referred
to as autoboxing.
We can also convert the object back into a primitive type:
Integer a = 1;
int b = a;
Here, the b primitive is assigned the value of a, which is 1. This operation of converting
a reference type back to a primitive is called unboxing. The compiler performs
autoboxing and unboxing automatically for us.
In addition to Integer, Java also provides the following wrapper classes for the following
primitives:
Note
The solution for this activity can be found on page 327.
}
3. Create an abstract method that returns the type of person in the hospital. Name
this method String getPersonType(), returning a String:
public abstract String getPersonType();
We have finished our abstract class and method. Now, we will continue to inherit
from it and implement this abstract method.
4. Create a new class called Doctor that inherits from the Person class:
public class Doctor extends Patient {
}
5. Override the getPersonType abstract method in our Doctor class. Return the "Arzt"
string. This is German for doctor:
@Override
public String getPersonType() {
return "Arzt";
}
6. Create another class called Patient to represent the patients in the hospital.
Similarly, make sure that the class inherits from Person and overrides the
getPersonType method. Return "Kranke". This is German for patient:
public class People extends Patient{
@Override
public String getPersonType() {
return "Kranke";
}
}
Now we have two classes, we will test our code using a third test class.
7. Create a third class called HospitalTest. We will use this class to test the two
classes we created previously.
8. Inside the HospitalTest class, create the main method:
public class HospitalTest {
public static void main(String[] args){
}
}
9. Inside the main method, create an instance of Doctor and another instance of
Patient:
Doctor doctor = new Doctor();
People people = new People();
10. Try calling the getPersonType method for each of the objects and print it out to the
console. What is the output?
String str = doctor.getPersonType();
String str1 = patient.getPersonType();
System.out.println(str);
System.out.println(str1);
The output is as follows:
Note
The solution for this activity can be found on page 329.
Note
The solution for this activity can be found on page 331.
Summary
In this lesson, we have learned that interfaces are a way for us to define a set of
methods that all classes implementing them must provide specific implementations for.
Interfaces can be used to implement events and listeners in your code when a specific
action occurs.
We then learned that typecasting is a way for us to change a variable of one type to
another type, as long as they are on the same hierarchy tree or implement a common
interface.
We also looked at the use of the instanceof operator and the Object class in Java, and
learned the concepts of autoboxing, unboxing, abstract classes, and abstract methods
in Java.
In the next lesson, we will look at a few common classes and data structures that come
with Java.
Learning Objectives
Introduction
This is the last topic in our discussion on OOP. So far, we have already looked at classes
and objects and how we can use classes as blueprints to create multiple objects. We saw
how we can use methods to hold the logic of our classes and fields to hold the state.
We've discussed how classes can inherit some properties from other classes to allow
easy reusability of code.
We've also looked at polymorphism, or how a class can redefine the implementation of a
method inherited from the superclass; and overloading, or how we can have more than
one method using the same name, as long as they have different signatures. We've also
discussed functions or methods.
We've looked at typecasting and interfaces in our previous lesson and how typecasting
is a way for us to change an object from one type to another, as long as they are on the
same hierarchy tree. We talked about upcasting and downcasting. Interfaces, on the
other hand, are a way for us to define generic behaviors that our classes can provide
specific implementations of their own.
In this section, we will look at a few common classes that come with Java. These are
classes that you will find yourself using on a daily basis, and therefore it's important
that you understand them. We will also talk about data structures and discuss common
data structures that come with Java. Remember that Java is a wide language and that
this list will not be exhaustive. Do find time to look at the official Java specification to
learn more about the other classes you have at your disposal. Throughout this lesson,
we will be introducing a topic, giving sample programs to illustrate the concepts, and
then we'll finish with an exercise.
A data structure is a way to store and organize data in order to facilitate access and
modifications. An example of a data structure is an array used to hold several items of
the same type or a map used to hold key-value pairs. No single data structure works
well for all purposes, and so it is important to know their strengths and limitations. Java
has a number of predefined data structures for storing and modifying different kinds of
data types. We will also cover some of them in the coming sections.
Sorting different types of data is a common task in a computer program.
Arrays
We touched upon arrays in Lesson 3, Control Flow, when we were looking at looping,
but it's worth taking an even closer look because they are powerful tools. An array
is a collection of ordered items. It is used to hold several items of the same type. An
example of an array in Java could be {1, 2, 3, 4, 5, 6, 7}, which is holding the
integers 1 through 7. The number of items in this array is 7. An array can also hold
strings or other objects as follows:
{"John","Paul","George", "Ringo"}
We can access an item from an array by using its index. An index is the location of the
item in the array. Elements in an array are indexed from 0. That is, the first number is
at index 0, the second number is at index 1, the third number is at index 2, and so on. In
our first example array, the last number is at index 6.
For us to be able to access an element from the array, we use myArray[0] to access the
first item in myArray, myArray[1] to access the second item, and so on to myArray[6] to
access the seventh item.
Java allows us to define arrays of primitive types and objects such as reference types.
Arrays also have a size, which is the number of items in that array. In Java, when we
create an array, we must specify its size. This size cannot be changed once the array has
been created.
We use the square brackets [ ] to indicate an array. In this example, we are creating
an array of integers that holds 10 items, indexed from 0 to 9. We specify the number of
items so that Java can reserve enough memory for the elements. We also use the new
keyword to indicate a new array.
For example, to declare array of 10 doubles, use this:
double[] myArray = new double[10];
You can also create an array and at the same time declare the items in the array
(initialization):
int[] myArray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Accessing Elements
To access array elements, we use the index enclosed in square brackets. For example,
to access the fourth element, we use myArray[3], to access the tenth element, we use
myArray[9].
Here's an example:
int first_element = myArray[0];
int last_element = myArray[9];
To get the length of the array, we use the length property. It returns an integer that is
the number of items in the array:
int length = myArray. length;
If the array has no items, length will be 0. We can use the length and a loop to insert
items into the array.
In this exercise, we used the first for loop to insert items into myArray and the second to
print out the items.
As we discussed previously, we can replace the second for loop with a for-each loop,
which is much shorter and makes the code easier to read:
for (int i : myArray) {
System.out.println(i);
}
Java does automatic bound checking for us - if you have created an array of size N and
use an index whose value is less than 0 or greater than N-1, your program will terminate
with an ArrayOutOfBoundsException exception.
Note
The solution for this activity can be found on page 335.
Note
The solution for this activity can be found on page 336.
Two-Dimensional Arrays
The arrays we have looked so far are referred to as one-dimensional because all the
elements can be considered to be on one row. We can also declare arrays that have
both columns and rows, just like a matrix or grid. Multidimensional arrays are arrays of
one-dimensional arrays we saw earlier. That is, you can consider one of the rows as a
one-dimensional array and then the columns are multiple one-dimensional arrays.
In java, to create a two-dimensional array, we use the double square brackets, [M]
[N ]. This notation creates a M-by-N array. We can then refer to an individual item in
the array by using the notation [ i ] [ j ] to access the element in the ith row and jth
column.
To create an 8-by-10 multidimensional array of doubles we do the following:
double[][] a = new double[8][10];
Java initializes all the numeric types to zeros and the Booleans to false. We could also
loop through the array and initialize each item manually to a value of our choice:
double[][] a = new double[8][10];
for (int i = 0; i < 8; i++)
for (int j = 0; j < 10; j++)
a[i][j] = 0.0;
Most of the rest of the operations with arrays remain pretty much the same
as with one-dimensional arrays. One important detail to remember is that in a
multidimensional array, using a[i] returns a row that is a one-dimensional array. You
have to use a second index to access the exact location you wish, a[i][j].
Note
Java also allows you to create higher-order dimensional arrays, but dealing with
them becomes complex. This is because our human brain can easily comprehend
three-dimensional arrays but higher-order ones become hard to visualize.
4. Create three for loops nested within each other, in order to write values into the
three-dimensional array:
for(i=0; i<2; i++)
{
for(j=0; j<2; j++)
{
for(k=0; k<2; k++)
{
arr[i][j][k] = no;
no++;
}
}
}
5. Print the elements out of the array using the three for loops that are nested within
each other:
for(i=0; i<2; i++)
{
for(j=0; j<2; j++)
{
for(k=0; k<2; k++)
{
System.out.print(arr[i][j][k]+ "\t");
}
System.out.println();
}
System.out.println();
}
}
}
}
}
}
In the following code, we can see how to use the Arrays class and a few methods that
we have at our disposal. All the methods are explained after the snippet:
import java.util.Arrays;
class ArraysExample {
public static void main(String[] args) {
double[] myArray = {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0};
System.out.println(Arrays.toString (myArray));
Arrays.sort(myArray);
System.out.println(Arrays.toString (myArray));
Arrays.sort(myArray);
int index = Arrays.binarySearch(myArray,7.0);
System.out.println("Position of 7.0 is: " + index);
}
}
In this program, we have three example uses of the Arrays class. In the first example,
we see how we can use Arrays.toString() to easily print out the elements of an array
without the need of the for loop we were using earlier. In the second example, we saw
how we can use Arrays.sort() to quickly sort an array. If we were to implement such
a method on our own, we would use many more lines and be prone to making a lot of
errors in the process.
In the last example, we sort the arrays and then search for 7.0 by using Arrays.
binarySearch(), which uses a searching algorithm called binary search.
Note
Arrays.sort() uses an algorithm called double-pivot quicksort to sort large
arrays. For smaller arrays, it uses a combination of Insertion sort and Merge sort.
It is better to trust that Arrays.sort() is optimized to each use case instead
of implementing your own sorting algorithm. Arrays.binarySearch() uses an
algorithm called binary search to look for an item in the array. It first requires that
the array be sorted, and that is why we called Arrays.sort() first. Binary search
splits the sorted array into two equal halves recursively until it can no longer divide
the array, at which point that value is the answer.
Insertion sort
Sorting is one of the fundamental applications of algorithms in computer science.
Insertion sort is a classic example of a sorting algorithm, and although it is inefficient
it is a good starting point when looking at arrays and the sorting problem. The steps in
the algorithm are as follows:
1. Take the first element in the array and assume it is already sorted since it is only
one.
2. Pick the second element in the array. Compare it with the first element. If it is
greater that the first element, then the two items are already sorted. If it is smaller
than the first element, swap the two elements so that they are sorted.
3. Take the third element. Compare it with the second element in the already
sorted subarray. If smaller then swap the two. The compare it again with the first
element. If it is smaller, then swap the two again so that it is the first. The three
elements will now be sorted.
4. Take the fourth element and repeat this process, swapping if it smaller than its left
neighbor, otherwise leaving it where it is.
5. Repeat this process for the rest of the items in the array.
6. The resultant array will be sorted.
Example
Take the array [3, 5, 8, 1, 9]:
1. Let's take the first element and assume it is sorted: [3].
2. Take the second element, 5. Since it is greater than 3, we leave the array as it is:
[3, 5].
3. Take the third element, 8. It is greater than 5, so there's no swapping here either:
[3, 5, 8].
4. Take the fourth element, 1. Since it is smaller than 8, we swap 8 and 1 to have: [3,
5, 1, 8].
5. Since 1 is still smaller than 5, we swap the two again: [3, 1, 5, 8].
6. 1 is still smaller than 3. We swap again: [1, 3, 5, 8].
5. Define the integer num as the length of the array in the sort() method:
int num = arr.length;
6. Create a for loop that executes until i has reached the length of the array. Inside
the loop, create the algorithm that compares the numbers: k will be an integer
defined by the index i, and j will be index i-1. Add a while loop inside the for loop
that switches the integers at i and i-1 with the following conditions: j is greater
or equal to 0 and the integer at index j is greater than k:
for (int i = 1; i < num; i++) {
int k = arr[i];
int j = i - 1;
while (j>= 0 && arr[j] > k) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = k;
}
}
The completed code looks as follows:
import java.util.Arrays;
public class InsertionSort {
public static void sort(int[] arr) {
int num = arr.length;
for (int i = 1; i < num; i++) {
int k = arr[i];
int j = i - 1;
while (j>= 0 && arr[j] > k) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = k;
}
}
public static void main(String[] args) {
int[] arr = {1, 3, 354, 64, 364, 64, 3, 4, 74, 2, 46};
System.out.println("Array before sorting is as follows: ");
System.out.println(Arrays.toString(arr));
sort(arr);
System.out.print("Array after sort looks as follows: ");
for (int i : arr) {
Java makes it easy for us to deal with commonly used data structures such as lists,
stacks, queues, and maps. It comes with the Java collections framework that provides
easy-to-use APIs when dealing with such data structures. A good example is when
we want to sort the elements in an array or want to search for a particular element
in the array. Instead of rewriting such methods from scratch on our own, Java comes
with methods that we can apply to our collections, as long as they conform to the
requirements of the collections framework. The classes of the collections framework
can hold objects of any type.
We will now look at a common class in the collections framework called ArrayList.
Sometimes we wish to store elements but are not sure of the number of items we are
expecting. We need a data structure to which we can add as many items as we wish and
remove some when we need to. The arrays we have seen so far require us to specify the
number of items when creating it. After that, we cannot change the size of that array
unless we create a whole new array. An ArrayList is a dynamic list that can grow and
shrink as needed; they are created with an initial size and when we add or remove an
item, the size is automatically enlarged or shrank as needed.
//Initial size of 5
ArrayList<Integer> myArrayList1 = new ArrayList<>(5);
myArrayList1.add(5);System.out.println("Size of myArrayList1: "+myArrayList1.
size());
//List of Person objectsArrayList<Person> people = new ArrayList<>();
people.add(john);System.out.println("Size of people: "+people.size());
}
}
Note
Inserting an object at an index less that 0 or greater than the size of the array list
will result in an IndexOutOfBoundsException and your program will crash. Always
check the size of the list before specifying the index to insert.
Here we are replacing the element at index 2 with a new Integer object with a value of
3. This method also throws IndexOutOfBoundsException if we try to replace the element
at an index greater than the size of the list or an index below zero.
If you also wish to remove a single element or all of the elements, ArrayList supports
that too:
//Remove at element at index 1
myArrayList1.remove(1);
System.out.println("Elements of myArrayList1 after removing the element: "
+myArrayList1.toString());
//Remove all the elements in the list
myArrayList1.clear();
System.out.println("Elements of myArrayList1 after clearing the list: "
+myArrayList1.toString());
To get an element at a specific index, use the get() method, passing in the index. The
method returns an object:
myArrayList1.add(10);
Integer one = myArrayList1.get(0);
System.out.println("Element at given index: "+one);
The output is as follows:
This method will also throw IndexOutOfBoundsException if the index passed is invalid.
To avoid the exception, always check the size of the list first. Consider the following
example:
Integer two = myArrayList1.get(1);
Iterators
The collections framework also provides iterators that we can use to loop through the
elements of an ArrayList. Iterators are like pointers to the items in the list. We can
use iterators to see if there is a next element in the list and then retrieve it. Consider
iterators as loops for the collections framework. We can use the array.iterator()
object with hasNext() to loop through an array.
In this class, we created a new ArrayList holding strings. We then inserted a few names
and created an iterator called citiesIterator. Classes in the collections framework
support the iterator() method, which returns an iterator to use with the collection.
The iterator has the hasNext() method, which returns true if there is another element in
the list after where we currently are, and a next() method that returns that next object.
next() returns an object instance and then implicitly downcasts it to a string because
our citiesIterator was declared to hold string types: Iterator<String> citiesIterator.
Instead of using iterators for looping, we can also use a normal for loop to achieve the
same goal:
for (int i = 0; i < cities.size(); i++){
String name = cities.get(i);
System.out .println(name);
}
Here, we are using the size() method to check the size of the list and get() to retrieve
an element at a given index. There is no need to cast the object to string as Java already
knows we are dealing with a list of strings.
Similarly, we can use a for-each loop, which is more concise but achieves the same goal:
for (String city : cities) {
System.out.println(city);
}
The output is as follows:
Note
ArrayList is an important class to know, as you will find yourself using it in your
day-to-day life. The class has more capabilities not covered here, such as swapping
two elements, sorting the items, and much more.
Note
The solution for this activity can be found on page 338.
Strings
Java has the string data type, which is used to represent a sequence of characters.
String is one of the fundamental data types in Java and you will encounter it in almost
all programs.
A string is simply a sequence of characters. "Hello World", "London", and "Toyota" are
all examples of strings in Java. Strings are objects in Java and not primitive types. They
are immutable, that is, once they are created, they cannot be modified. Therefore, the
methods we will consider in the following sections only create new string objects that
contain the result of the operation but don't modify the original string object.
Creating a String
We use double quotes to denote a string, compared to single quotes for a char:
public class StringsDemo {
public static void main(String[] args) {
String hello="Hello World";
System.out.println(hello);
}
}
The hello object is now a string and is immutable. We can use delimiters in strings,
such as \n to represent a newline, \t to present a tab, or \r to represent a return:
String data = '\t'+ "Hello"+ '\n'+" World";
System.out.println(data);
The output is as follows:
We have a tab before Hello and then a newline before World, which prints World on the
next line.
Concatenation
We can combine more than one string literal in a process commonly referred to as
concatenation. We use the + symbol to concatenate two strings as follows:
String str = "Hello " + "World";
System.out.println(str);
The output is as follows:
Hello World
Concatenation is often used when we want to substitute a value that will be calculated
at runtime. The code will look as follows:
String userName = getUserName(); // get the username from an external
location like database or input field
System.out.println( " Welcome " + userName);
In the first line, we get userName from a method that we haven't defined here. Then we
print out a welcome message, substituting the userName with userName we got earlier.
Concatenation is also important when we want to represent a string that spans more
than one line:
String quote = "I have a dream that " +
"all Java programmers will " +
"one day be free from " +
"all computer bugs!";
System.out.println(quote);
Here is the output:
In addition to the + symbol, Java also provides the concat() method for concatenating
two string literals:
String wiseSaying = "Java programmers are " . concat("wise and
knowledgeable").concat("." );
System.out.println(wiseSaying);
Here is the output:
To access a character at a given index, use the charAt(i). This method takes the index
of the character you want and returns a char of it:
char c = quote.charAt(7);
System.out.println(c);
Here is the output:
r
Activity 24: Input a String and Output Its Length and as an Array
In order to check that names being inputted into a system aren't too long, we can
use some of the features mentioned previously to count the length of a name. In this
activity, you will write a program that will input a name and then export the length of
the name and the first initial.
The steps are as follows:
1. Import the java.util.Scanner package.
2. Create a public class called nameTell and a main method.
3. Use the Scanner and nextLine to input a string at the prompt "Enter your name:".
4. Count the length of the string and find the first character.
5. Print the output as follows:
Your name has 10 letters including spaces.
The first letter is: J
The output will be as follows:
Note
The solution for this activity can be found on page 340.
Note
The solution for this activity can be found on page 341.
Conversion
Sometimes we might wish to convert a given type to a string so we can print it out,
or we might want to convert a string to a given type. An example is when we wish to
convert the string "100" to the integer 100, or convert the integer 100 to string "100".
Concatenating a primitive data type to a string using the + operator will return a string
representation of that item.
Here we used the parseInt() method to get the integer value of the string, and then
used the toString() method to convert the integer back to a string.
To convert an integer to a string, we concatenate it with an empty String "":
int a = 100;
String str = "" + a;
The output is as follows:
100
Note
Every object in Java has a string representation. Java provides the toString()
method in the Object superclass, which we can override in our classes to provide a
string representation of our classes. String representations are important when we
want to print our class in string format.
Return true if this string ends with or begins with a given substring:
boolean value= data.endsWith( "ne");
System.out.println(value);
boolean value1 = data.startsWith("He");
System.out.println(value);
StringBuilder
We have stated that strings are immutable, that is, once they are declared they
cannot be modified. However, sometimes we wish to modify a string. In such cases,
we use the StringBuilder class. StringBuilder is just like a normal string except it is
modifiable. StringBuilder also provides extra methods, such as capacity(), which
returns the capacity allocated for it, and reverse(), which reverses the characters in it.
StringBuilder also supports the same methods in the String class, such as length() and
toString().
In this exercise, we created a new instance of StringBuilder with the default capacity
of 16. We then inserted a few strings and then printed out the entire string. We also got
the number of characters in the builder by using length(). We then got the capacity of
StringBuilder. The capacity is the number of characters allocated for StringBuilder.
It is usually higher than or equal to the length of the builder. We finally reversed all
the characters in the builder and then print it out. In the last print out, we didn't use
stringBuilder.toString() because Java implicitly does that for us.
11. Close the inner for loop and go inside the first for loop.
12. Check if isDuplicate is false. If it is, then append c to result.
13. Go outside the first for loop and return the result. That concludes our algorithm.
14. Go back to our empty main method. Create a few test strings of the following:
aaaaaaa
aaabbbbb
abcdefgh
Ju780iu6G768
15. Pass the strings to our method and print out the result returned from the method.
16. Check the result. Duplicate characters should be removed in the returned strings.
The output should look like this:
Note
The solution for this activity can be found on page 342.
Summary
This lesson brings us to the end of our discussion on the core principles of object-
oriented programming. In this lesson, we have looked at data types, algorithms, and
strings.
We've seen how an array is an ordered collection of items of the same type. Arrays are
declared with square brackets, [ ], and their size cannot be modified. Java provides
the Arrays class from the collections framework that has extra methods we can use on
arrays.
We also saw the concept of Arraylist and string. Java provides the StringBuilder class,
which is basically a modifiable string. stringbuilder has length and capacity functions.
Introduction
In previous lessons, you learned how objects can be grouped together in arrays to
help you process data in batches. Arrays are really useful but the fact that they have a
static length makes them hard to deal with when loading an unknown amount of data.
Also, accessing objects in the array requires you to know the array's index, otherwise
traversing the whole array is necessary to find the object. You also learned briefly about
ArrayList, which behaves like an array that can dynamically change its size to support
more advanced use cases.
In this lesson, you'll learn how ArrayList actually works. You'll also learn about the Java
Collections Framework, which includes some more advanced data structures for some
more advanced use cases. As part of this journey, you'll also learn how to iterate on
many data structures, compare objects in many different ways, and sort collections in
an efficient way.
You'll also learn about generics, which is a powerful way of getting help from the
compiler on using collections and other special classes.
CSV Files
A comma-separated value (CSV) file is a very common type of text file that is used to
transport data between systems. CSVs are useful because they are easy to generate and
easy to read. The structure of such a file is very simple:
• One record per line.
• The first line is the header.
• Each record is a long string where values are separated from others using a
comma (values can also be separated by other delimiters).
The following is a piece of a file that was extracted from the sample data we'll be using:
id,name,email
10,Bill Gates,[email protected]
30,Jeff Bezos,[email protected]
20,Marc Benioff,[email protected]
6. Finally, you split the line using the split method from the String class. That
method receives a separator, which in our case is a comma:
String [] split = line.split(",");
System.out.printf("%d - %s\n", lineCounter, split[1]);
Note
You can see how FileReader is passed into BufferedReader and then never
accessed again. That's because we only want the lines and we don't care about the
intermediate process of transforming characters into lines.
Congratulations! You wrote an application that can read and parse a CSV. Feel free to
dig deeper into this code and understand what happens when you change the initial
line count value.
The output is as follows:
1 - Bill Gates
2 - Jeff Bezos
3 - Marc Benioff
4 - Bill Gates
5 - Jeff Bezos
6 - Sundar Pichai
7 - Jeff Bezos
8 - Larry Ellison
9 - Marc Benioff
10 - Larry Ellison
11 - Jeff Bezos
12 - Bill Gates
13 - Sundar Pichai
14 - Jeff Bezos
15 - Sundar Pichai
16 - Marc Benioff
17 - Larry Ellison
18 - Marc Benioff
19 - Jeff Bezos
20 - Marc Benioff
21 - Bill Gates
22 - Sundar Pichai
23 - Larry Ellison
24 - Bill Gates
25 - Larry Ellison
26 - Jeff Bezos
27 - Sundar Pichai
Figure 7.2: CSVReader can be added to the chain to read records one by one
4. Create a constructor that receives BufferedReader and set it to the field. This
constructor will also read and discard the first line of the passed-in reader, since
that is the header and we don't care about them in this lesson:
public CSVReader(BufferedReader reader) throws IOException {
this.reader = reader;
return line.split(",");
}
Note
In a more elaborate implementation, we could store the header to expose extra
functionalities for the user of the class, such as fetch value by header name. We
could also do some tidying and validation on the line to ensure no extra spaces are
wrapping the values and that they contain the expected amount of values (same as
the header count).
Note
From the preceding snippet, you can see that your code is now much simpler. It's
focused on delivering the business logic (printing the second value with line count)
and doesn't care about reading a CSV. This is a great practical example of how to
create your readers to abstract away logic about processing the data coming from
files.
10. For the code to compile, you'll need to add the imports from the java.io package:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
Arrays
As you have already learned from previous lessons, arrays are really powerful, but their
static nature makes things difficult. Suppose you have a piece of code that loads users
from some database or CSV file. The amount of data that will come from the database or
file is unknown until you finish loading all the data. If you're using an array, you would
have to resize the array on each record read. That would be too expensive because
arrays can't be resized; they need to be copied over and over.
To be more efficient, you could initialize the array with a specified capacity and trim the
array after finishing reading all the records to ensure that it doesn't contain any extra
empty rows in it. You would also need to ensure that the array has enough capacity
while you're adding new records into it. If not, you'll have to make a new array with
enough room and copy data over.
2. At the beginning of the User class, add a static method that will create a user from
values coming as an array of strings. This will be useful when creating a User from
the values read from a CSV:
public static User fromValues(String [] values) {
int id = Integer.parseInt(values[0]);
String name = values[1];
String email = values[2];
return new User(id, name, email);
}
3. Create another file called IncreaseOnEachRead.java and add a class with the same
name and a main method that will pass the first argument from the command line
to another method called loadUsers. Then, print the number of users loaded, like
so:
public class IncreaseOnEachRead {
public static final void main (String [] args) throws Exception {
User[] users = loadUsers(args[0]);
System.out.println(users.length);
}
}
4. In this same file, add another method called loadUsers, which will return an array
of users and receive a String called fileToRead, which will be the path to the CSV
file to read:
public static User[] loadUsers(String fileToReadFrom) throws Exception {
5. In this method, start by creating an empty users array and returning it at the end:
User[] users = new User[0];
return users;
6. Between those two lines, add the logic to read the CSV record by record using
your CSVReader. For each record, increase the size of the array by one and then add
a newly created User to the last position on the array:
BufferedReader lineReader = new BufferedReader(new
FileReader(fileToReadFrom));
try (CSVReader reader = new CSVReader(lineReader)) {
String [] row = null;
while ( (row = reader.readRow()) != null) {
// Increase array size by one
// Create new array
User[] newUsers = new User[users.length + 1];
users[users.length - 1] = User.userFromRow(row);
}
}
The output is as follows:
27
You now can read from the CSV file and have a reference to all users loaded from it.
This implements the approach of increasing the array on each record read. How would
you go about implementing the more efficient approach of initializing the array with
some capacity and increasing it as needed and trimming it at the end?
Activity 27: Read Users from CSV Using Array with Initial Capacity
In this activity you're going to read users from the CSV similar to how you did in the
previous exercise, but instead of growing the array on every read, create the array with
an initial capacity and grow it as necessary. At the end, you'll need to check if the array
has empty spaces left and shrink it to return an array with exact size as the number of
users loaded.
To complete this activity you'll need to:
1. Initialize an array with an initial capacity.
2. Read the CSV from the path passed in from the command line in a loop, create
users and add them to the array.
3. Keep track of how many users you loaded in a variable.
4. Before adding Users to the array, you'll need to check the size of the array and
grow it if necessary.
5. At the end, shrink the array as necessary to return the exact number of users
loaded.
Note
The solution for this activity can be found on page 345.
Vectors
Vectors solve the problem of arrays being static. They provide a dynamic and scalable
way of storing many objects. They grow as you add new elements, can be prepared to
receive large numbers of elements, and it is easy to iterate over elements.
To take care of the internal array without having to resize it unnecessarily, a vector
initializes it with some capacity and keeps track of what position the last element was
added to using a pointer value, which is just an integer that marks that position. By
default, the initial capacity is 10. When you add more than the capacity of the array, the
internal array is copied over to a new one that is bigger by some factor, leaving more
empty space open so that you can add extra elements. The copying process is just like
you did manually with the array in Exercise 24: Reading Users from a CSV File into an
Array. The following is an illustration of how that works:
Using vectors was the way to get dynamic arrays in Java before the Java Collections
Framework. However, there were two major problems:
• Lack of a defined interface that was easy to understand and extend
• Fully synchronized, which means it is protected against multi-threaded code
After the Java Collections Framework, vectors were retrofitted to comply with the new
interfaces, solving the first problem.
4. Add the imports that are required for this file to compile:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Vector;
5. Create a file called ReadUsersIntoVector.java and add a class with the same name
and a main method in it:
public class ReadUsersIntoVector {
public static void main (String [] args) throws IOException {
}
}
6. In the main method, similar to what we did in the array case, call the method that
loads users from a CSV into Vector and then print the size of Vector. In this case,
use the loadUsersInVector() method we created in the previous step:
Vector users = UserLoader.loadUsersInVector(args[0]);
System.out.println(users.size());
7. Add the imports for this file to compile:
import java.io.IOException;
import java.util.Vector;
The output is as follows:
27
Congratulations on finishing one more exercise! This time, you can see that your code is
much simpler since most of the logic of loading the CSV, splitting it into values, creating
a user, and resizing arrays is now abstracted away.
You can read more about the format of the data in the website or just by opening it as a
text file. Two things to keep in mind while working with this file:
• There's an extra empty line at the end of the file
• This file has no header line
Create an application that will calculate the minimum, maximum and average wage
in this file. After reading all rows, your application should print these results. To
accomplish this you'll need to:
1. Load all wages from the file into a Vector of integers using your CSVReader. You can
modify your CSVReader to support files without headers.
2. Iterate over the values in the Vector and keep track of three values: minimum,
maximum and sum.
3. Print the results at the end. Remember, the average is just the sum divided by the
size of the Vector.
Note
The solution for this activity can be found on page 347.
When you need to iterate over a vector, you can use the loop with an index, just like an
array:
for (int i = 0; i < values.size(); i++) {
String value = (String) values.get(i);
System.out.printf("%d - %s\n", i, value);
}
You can also use Vector in a for-each loop, just like an array:
for (Object value : values) {
System.out.println(value);
}
This works because Vector implements Iterable. Iterable is a simple interface that tells
the compiler that the instance can be used in a for-each loop. In fact, you could change
your CSVReader to implement Iterable and then use it in a for-each loop, just like in the
following code:
try (IterableCSVReader csvReader = new IterableCSVReader(reader)) {
for (Object rowAsObject : csvReader) {
User user = User.fromValues((String[]) rowAsObject);
System.out.println(user.name);
}
}
Iterable is a very simple interface; it has only one method that you need to implement:
iterator(). That method returns an iterator. An iterator is another simple interface that
only has two methods to implement:
• hasNext(): Returns true if the iterator still has elements to return.
• next(): Fetches the next record and returns it. It will throw an exception if
hasNext() returns false before calling this.
An iterator represents a simple way of getting things out of a collection. But it also has
another method that is important in some more advanced contexts, remove(), which
removes the current element that was just fetched from calling next().
This remove method is important because when you're iterating on a collection, you
cannot modify it. This means that if you write a for-each loop to read elements from
the vector and then inside this loop you call remove(Object) to remove an element from
it, ConcurrentModificationException would be thrown. So, if you want to iterate over
a collection using a loop and in this loop you need to remove an element from vector,
you'll have to use an iterator.
You must be thinking, "why would it be designed to work like this?" Because Java is a
multi-threaded language. You won't learn how to create threads or use them in this
book because it's an advanced topic. But the idea behind multi-threading is that a piece
of data in memory can be accessed by two pieces of code at the exact same time. This is
possible because of the multi-core capabilities of modern computers. With collections
and arrays, you have to be very careful when working on multi-threaded applications.
The following is an illustration of how that happens:
Note
The solution for this activity can be found on page 349.
Hashtable
Arrays and vectors are great when dealing with many objects that are to be processed
in sequence. But when you have a group of objects that need to be indexed by a key, for
example, some kind of identification, then they become cumbersome.
Enter hashtables. They are a very old data structure that was created to solve exactly
this problem: given a value, quickly identifying it and finding it in an array. To solve this,
hash tables use a hashing function to uniquely identify objects. From that hash, they can
use another function (normally a remainder of a division) to store the values in an array.
That makes the process of adding an element to the table deterministic and fetching it
very fast. The following is an illustration of the process of how a value gets stored in a
hashtable:
Figure 7.5: The process behind storing and fetching a value from a hash table
A hashtable uses an array to internally store an entry, which represents a key-value pair.
When you put a pair in the hashtable, you provide the key and the value. The key is used
to find where in the array the entry will be stored. Then, an entry holding the key and
value is created and stored in the position specified.
To fetch the value, you pass in the key from which the hash is calculated and the entry
can be quickly found in the array.
An interesting feature you get for free from this process is de-duplication. Because
adding a value with the same key will generate the same hash, when you do that, it will
overwrite whatever was stored in there previously.
Just as with vectors, the Hashtable class was added to Java before the Collections
Framework. It suffered from the same two problems that vectors suffered from: lack
of defined interfaces and being fully synchronized. It also breaks the Java naming
convention by not following CamelCase for word separation.
Also, as with vectors, after the introduction of the Collections Framework, hashtables
was retrofitted to comply with the new interfaces, making them a seamless part of the
framework.
4. Create a file called FindUserHashtable.java and add a class with the same name
and add a main method:
public class FindUserHashtable {
public static void main(String [] args) throws IOException {
}
}
5. In your main method, load the users into a Hashtable using the method we created
in the previous steps and print the number of users found:
Hashtable users = UsersLoader.loadUsersInHashtableByEmail(args[0]);
System.out.printf("Loaded %d unique users.\n", users.size());
6. Print some text to inform the user that you're waiting for them to type in an email
address:
System.out.print("Type a user email: ");
7. Read the input from the user by using Scanner:
try (Scanner userInput = new Scanner(System.in)) {
String email = userInput.nextLine();
8. Check whether the email address is in Hashtable. If not, print a friendly message
and exit the application:
if (!users.containsKey(email)) {
// User email not in file
System.out.printf("Sorry, user with email %s not found.\n", email);
return;
}
9. If found, print some information about the user that was found:
User user = (User) users.get(email);
System.out.printf("User with email '%s' found!", email);
System.out.printf(" ID: %d, Name: %s", user.id, user.name);
10. Add the necessary imports:
import java.io.IOException;
import java.util.Hashtable;
import java.util.Scanner;
3. For each entry, print the minimum, maximum and average wages for each
education level found in the file.
Note
The solution for this activity can be found on page 351.
Generics
Classes that work with other classes in a generic way, like Vector, didn't have a way to
explicitly tell the compiler that only one type was accepted. Because of that, it uses
Object everywhere and runtime checks like instanceof and casting were necessary
everywhere.
To solve this problem, Generic was introduced in Java 5. In this section you'll
understand better the problem, the solution and how to use it.
/* If you uncomment the last line and try to compile, you would get the
following error: */
Let's say you try to do something similar with Vector, like the following:
Vector usersVector = new Vector();
usersVector.add(user); // This compiles
usersVector.add("Not a user"); // This also compiles
The compiler will not help you at all. The same thing goes for Hashtable:
Hashtable usersTable = new Hashtable();
usersTable.put(user.id, user); // This compiles
usersTable.put("Not a number", "Not a user"); // This also compiles
This also occurs when fetching data. When fetching from an array, the compiler knows
what type of data is in there, so you don't need to cast it:
User userFromArray = usersArray[0];
To fetch data from a collection, you need to cast data. A simple example is adding the
following code after adding the two elements to the previous usersVector:
User userFromVector = (User) usersVector.get(1);
This was a big source of bugs for a long time in the Java world. And then generics came
along and changed everything.
Generics is a way for you to tell the compiler that a generic class will only work with a
specified type. Let's have a look at what this means:
• Generic class: A generic class is a class that has a generic functionality which
works with different types, like a Vector, that can store any type of object.
• Specified type: With generics, when you instantiate a generic class, you specify
what type that generic class will be used with. For example, you can specify that
you only want to store users in your Vector.
• Compiler: It is important to highlight that a generic is a compile time-only feature.
There's no information about generic type definition at runtime. At runtime,
everything behaves like it was before generics.
Generic classes have a special declaration that exposes how many types it requires.
Some generic classes require multiple types, but most only require one. In the Javadocs
for generic classes, there's a special angle brackets arguments list that specifies how
many type parameters it requires, such as in <T, R>. The following is a screenshot of the
Javadoc for java.util.Map, which is one of the interfaces in the Collections Framework:
Figure 7.6: Screenshot of the Javadoc for java.util.Map, where it shows the generic type declaration
For a hashtable, you need to specify the types for the key and value. For a hashtable that
would store users with their IDs as keys, the declaration would look as follows:
Hashtable<Integer, User> usersTable = new Hashtable<>();
Just declaring the generic types with the correct parameters will solve the problems
we described earlier. For example, let's say you are declaring a vector so that it only
handles users. You would try and add a String to it, as in the following code:
usersVector.add("Not a user");
Now that the compiler ensures that nothing except users will be added to the vector,
you can fetch data from it without having to cast it. The compiler will automatically
convert the type for you:
// No casting needed anymore
User userFromVector = usersVector.get(0);
Note:
You don't have to change other places where you called these methods because
using them as the non-generic version still works.
2. Create a file named FindByStringWithGenerics.java and add a class with the same
name and a main method, like so:
public class FindByStringWithGenerics {
public static void main (String [] args) throws IOException {
}
}
3. Add a call to the loadUsersInVector method to your main method, storing the value
in a vector with the specified generic type. Print the number of users loaded:
Vector<User> users = UsersLoader.loadUsersInVector(args[0]);
System.out.printf("Loaded %d users.\n", users.size());
4. After that, ask the user to type a string and store that in a variable after
transforming it to lowercase:
System.out.print("Type a string to search for: ");
// Read user input from command line
try (Scanner userInput = new Scanner(System.in)) {
String toFind = userInput.nextLine().toLowerCase();
}
5. Inside the try-with-resource block, create a variable to count the number of users
found. Then, iterate over the users from the vector we loaded previously and
search for the string in the email and name for each user, making sure to set all
strings to lowercase:
int totalFound = 0;
for (User user : users) {
if (user.email.toLowerCase().contains(toFind)
||user.name.toLowerCase().contains(toFind)) {
System.out.printf("Found user: %s",user.name);
System.out.printf(" Email: %s\n", user.email);
totalFound++;
}
}
6. Finally, if totalFound is zero, meaning no users were found, print a friendly
message. Otherwise, print the number of users you found:
if (totalFound == 0) {
System.out.printf("No user found with string '%s'\n", toFind);
} else {
System.out.printf("Found %d users with '%s'\n", totalFound, toFind);
}
When you go to read the String Javadoc, its compareTo method says that it "compares
two strings lexicographically". This means that it uses the character code to check
which string comes first. The difference here is that the character codes have all the
uppercase letters first, then all the lowercase ones. Because of that, "A" comes after "B",
since B's character code is before A's.
But what if we want to compare strings alphabetically and not lexicographically? As
mentioned before, objects can be compared in many different spectrums. Because of
that, Java provides another interface that can be used to compare two objects: java.
util.Comparator. Classes can implement a comparator using the most common use
case, like numbers can be compared using their natural order. Then, we can create
another class that implements Comparator to compare objects using some other custom
algorithm.
Sorting
When you have collections of objects, it's very common to want to sort them in some
way or other. Being able to compare two objects is the basis for all sorting algorithms.
Now that you know how to compare objects, it's time to use that to add sorting logic to
your applications.
There are many sorting algorithms out there, each one with its own strengths and
weaknesses. For simplicity, we'll discuss only two: bubble sort, because of its simplicity,
and merge sort, because of its stable performance, which is why it was picked by the
Java core implementers.
Bubble Sort
The most naive sorting algorithm is bubble sort, but it's also the simplest to understand
and implement. It works by iterating over each element and comparing it with the next
element. If it finds two elements that are not sorted, it swaps them and moves on to the
next. When it gets to the end of the array, it checks how many elements were swapped.
It continues this cycle until the number of swapped elements in a cycle is zero, which
means that the whole array or collection has been sorted.
The following is an illustration of how sorting an array with seven elements using
bubble sort would happen:
Bubble sort is very space efficient since it doesn't need any extra arrays or a place to
store variables. However, it uses a lot of iterations and comparisons. In the example
from the illustration, there's a total of 30 comparisons and 12 swaps.
Merge Sort
Bubble sort works, but as you may have noticed, it is really naive and it feels like there
are a lot of wasted cycles. Merge sort, on the other hand, is much more efficient and is
based on the divide-and-conquer strategy. It works by recursively splitting the array/
collection in half until you end up with multiple pairs of one element. Then, it merges
them back together while sorting at the same time. You can see how this works in the
following illustration:
In comparison to bubble sort, the number of comparisons for merge sort is much
smaller – only 13 for the illustrated example. It uses more memory space since every
merge step needs an extra array to store the data that is being merged.
One good thing that is not explicit in the preceding illustration is that merge sort has
stable performance since it will always execute the same amount of steps; it doesn't
matter how shuffled or sorted the data is. Compared to bubble sort, the number of
swaps can get very high if you get a situation where the array/collection is sorted
backwards.
Stability is very important for a core library such as the Collections Framework, and
that's why merge sort was the algorithm that was picked as the implementation for
sorting in the java.util.Collections utility class.
Note
The solution for this activity can be found on page 354.
Data Structures
The most fundamental part of building applications is processing data. The way
you store the data is influenced by the way you'll need to read and process it. Data
structures define the way you store data. Different data structures optimize for
different use cases. So far, we have mentioned two ways of accessing data:
• Sequentially, as with an array or vector
• Key-value pairs, as with a hashtable
Note
In the following sections, we'll discuss the basic data structure of interfaces that
have been added to the Collections Framework and how they differ from each
other. We'll also dive deeper into each implementation and the use cases they
solve.
Collection
This is the most generic interface that is the base for all collections except Map. The
documentation describes it as representing a collection of objects called elements.
It declares the basic interface for all collections with the following most important
methods:
• add(Element): Adds an element to the collection
• clear(): Removes all elements from the collection
• contains(Object): Checks whether an object is in the collection
• remove(Object): Removes the specified element from the collection, if present
• size(): Returns the number of elements stored in the collection
List
The list interface represents a sequential collection of elements that can grow
indefinitely. Elements in a list can be accessed by their index, which is the position that
they were put in, but can change if elements are added between other elements.
When iterating over a list, the order that the elements will be fetched in is deterministic
and will always be based on the order of their indexes, just like an array.
As we mentioned previously, Vector was retrofitted to support the Collections
Framework and it implements the list interface. Let's take a look at the other
implementations that are available.
List extends Collection, so it inherits all the methods we mentioned previously and
adds some other important methods, mostly associated with position-based access:
• add(int, Element): Adds an element at the specified position
• get(int): Returns the element at the specified position
• indexOf(Object): Returns the index of the object or -1 if not present in the
collection
• set(int, Element): Replaces the element at the specified position
• subList(int, int): Creates a sublist from the original list
ArrayList
Just like Vector, ArrayList wraps an array and takes care of scaling it as needed,
behaving just like a dynamic array. The major difference between the two is that vectors
are fully synchronized. This means that they protect you from concurrent access
(multi-threaded applications). It also means that on non-concurrent applications, which
occurs in the majority of the cases, Vector is slower because of the locking mechanisms
that are added to it. For that reason, it is recommended that you use ArrayList, unless
you really need a synchronized list.
As we mentioned previously, for all purposes, ArrayList and Vector can be used
interchangeably. Their functionality is the same and both implement the same
interfaces.
LinkedList
LinkedList is an implementation of List that does not store elements in an underlying
array, like ArrayList or Vector. It wraps each value in another object called a node. A
node is an internal class that contains two references to other nodes (the next node
and the previous node) and the value being stored for that element. This type of list is
known as a double-linked list because each node is linked twice, once in each direction:
from the previous to the next and from the next to the previous.
Internally, LinkedList stores a reference to the first and last nodes, so it can only
traverse the list starting from the beginning or the end. It is not good for random or
position-based access as with arrays, ArrayLists, and vectors, but it is good when adding
an undetermined number of elements very fast.
LinkedList also stores a variable that keeps track of the size of the list. That way, it
doesn't have to traverse the list every time to check the size.
The following illustration shows how LinkedList is implemented:
Map
When you need to store elements associated with keys, you use Maps. As we saw
previously, Hashtable is a powerful mechanism for indexing objects by some key,
and after the addition of the Collections Framework, Hashtable was retrofitted to
implement Map.
The most fundamental property of maps is that they cannot contain duplicate keys.
Maps are powerful because they allow you to see the dataset from three different
perspectives: keys, values, and key-value pairs. After adding your elements to a map,
you can iterate over them from any of those three perspectives, giving you extra
flexibility when fetching data from it.
The most important methods in the Map interface are as follows:
• clear(): Remove all keys and values from the map
• containsKey(Object): Check whether the key is present in the map
• containsValue(Object): Check whether the value is present in the map
• entrySet(): Return a set of entries with all the key-value pairs in the map
• get(Object): Return the value associated with the specified key if present
• getOrDefault(Object, Value): Return the value associated with the specified key if
present, otherwise return the specified value
• keySet(): A set containing all keys in the map
• put(Key, Value): Add or replace a key-value pair
• putIfAbsent(Key, Value): Same as the previous method, but won't replace if the
key is already present
• size(): The number of key-value pairs in this map
• values(): Return a collection with all the values present in this map
HashMap
Just like Hashtable, HashMap implements a hash table to store the entries of key-value
pairs, and it works exactly the same way. Just as Vector is to ArraySet, Hashtable is so
to HashMap. Hashtable existed before the Map interface, so HashMap was created as a
non-synchronous implementation of the hash table.
As we mentioned before, hash tables, and consequently HashMap, are very fast to find
elements by key. They are great to use as an in-memory cache where you load data
that's been keyed by some field, like you did in Exercise 26: Writing an Application that
Finds a User by Email.
TreeMap
TreeMap is an implementation of Map that can keep key-value pairs sorted by key or by a
specified comparator.
As the name implies, TreeMap uses a tree as the underlying storage mechanism. Trees
are very special data structures that are used to keep data sorted as insertions happen
and at the same time, fetch data with very few iterations. The following illustration
shows what a tree looks like and how a fetch operation can quickly find an element,
even in very large trees:
Trees have nodes that represent the branches. Everything starts from a root node and
expands into multiple branches. At the ends of the leaf nodes, there are nodes with no
children. TreeMap implements a specific type of tree called a red-black tree, which is a
binary tree, so each node can have only two children.
LinkedHashMap
The name of the LinkedHashMap class is a bit cryptic because internally it uses two data
structures to support some use cases that HashMap didn't support: a hash table and
a linked list. The hash table is used to quickly add and fetch elements from the map.
The linked list is used when iterating over the entries by whatever means: key, value,
or key-value pair. This gives it the ability to iterate over the entries in a deterministic
order, which is whatever order they were inserted in.
Set
The main characteristic of sets is that they contain no duplicate elements. Sets are
useful when you want to collect elements and at the same time eliminate duplicate
values.
Another important characteristic about sets is that the order that you fetch elements
from them varies based on the implementation. This means that if you want to eliminate
duplicates, you have to think about how you're going to read them afterward.
All set implementations in the Collections Framework are based on their corresponding
Map implementation. The only difference is that they handle the values in the set as the
keys in the map.
HashSet
By far the most common of all the sets, HashSet uses a HashMap as the underlying
storage mechanism. It stores its elements in a random order, based on the hashing
function used in HashMap.
TreeSet
Backed by a TreeMap, TreeSet is really useful when you want to store unique elements
sorted by their natural order (comparables) or using a comparator.
LinkedHashSet
Backed by LinkedHashMap, LinkedHashSet will keep the insertion order and remove
duplicates as you add them to the set. It has the same advantages as LinkedHashSet:
fast insertion and fetching like HashSet, and fast iteration like LinkedList.
Queue
Queues are a special data structure that respect the First In, First Out (FIFO) pattern.
This means that it keeps the elements in order of insertion and can return the elements
starting from the first inserted one while adding elements to the end. That way,
new work can be enqueued at the end of the queue while work to be processed gets
dequeued from the front. The following is an illustration of this process:
java.util.ArrayDeque
The implementation of Queue and Deque uses an array as the underlying data store.
The array grows automatically to support the data that's added to it.
java.util.PriorityQueue
The implementation of Queue uses a heap to keep elements in sort order. The order
can be given by the element if it implements java.lang.Comparable or by a passed-in
comparator. A heap is a specialized type of tree that keeps elements sorted, similar to
TreeMap. This implementation of queue is great for processing elements that need to be
processed in some priority.
2. Create another file called SendAllEmails.java with a class and a main method.
public class SendAllEmails {
3. Add a static field called runningProcess. This will represent the send email process
that is running:
private static Process runningProcess = null;
4. Create a static method that will try to initiate the process of sending an email by
dequeuing an element from the queue, if the process is available:
private static void sendEmailWhenReady(ArrayDeque<String> queue)
throws Exception {
// If running, return
if (runningProcess != null && runningProcess.isAlive()) {
System.out.print(".");
return;
}
System.out.print("\nSending email");
String email = queue.poll();
String classpath = System.getProperty("java.class.path");
String[] command = new String[]{
"java", "-cp", classpath, "EmailSender", email
};
runningProcess = Runtime.getRuntime().exec(command);
}
5. In the main method, create an ArrayDeque of strings that will represent the queue
of emails to send to:
ArrayDeque<String> queue = new ArrayDeque<>();
6. Open the CSV to read each row from it. You can do this by using CSVReader:
FileReader fileReader = new FileReader(args[0]);
BufferedReader bufferedReader = new BufferedReader(fileReader);
try (CSVReader reader = new CSVReader(bufferedReader)) {
String[] row;
while ( (row = reader.readRow()) != null) {
User user = User.fromValues(row);
}
}
7. With the user loaded, we can add its email to the queue and try to send an email
immediately:
queue.offer(user.email);
sendEmailWhenReady(queue);
8. Because reading from a file is, in general, very fast, we'll simulate a slow read by
adding some sleep time:
Thread.sleep(100);
9. Outside the try-with-resources block, that is, after we've finished reading all users
from the file, we need to ensure we drain the queue. For that, we can use a while
loop that runs while the queue is not empty:
while (!queue.isEmpty()) {
sendEmailWhenReady(queue);
Note
In this case, it is important to not use 100% of the CPU while you sleep. This is very
common when processing elements from a queue, like in this case.
10. Now you can just wait for the last send email process to finish, following a similar
pattern: check and wait while sleeping:
while (runningProcess.isAlive()) {
System.out.print(".");
Thread.sleep(100);
}
System.out.println("\nDone sending emails!");
Congratulations! You wrote an application that simulates the sending of emails using
constrained resources (one process only). This application is ignoring the fact that users
are duplicated in the file. It also ignores the output of the send email process. How
would you implement a duplicate send detector and avoid that issue? How do you think
the output of the send process affects the decision of duplicate avoidance?
Properties of Collections
When picking a data structure to solve a problem, you'll have to consider the following
things:
• Ordering - If order is important when accessing the data, what order the data will
be accessed?
• Uniqueness - Does it matter if you have the same element multiple times inside
the collection? How do you define uniqueness?
• Nullables - Can values be null? If mapping key to values, is the null key valid? Does
it make sense to use null in either?
Use the following table to determine what collection better suits your use case:
Note
"Sorted naturally" means that it will sort based on the element (or key) if the
element implements Comparable or using a passed-in comparator.
Summary
When developing applications, processing data is one of the most fundamental tasks.
In this lesson, you learned how to read and parse data from files so that you're able to
process them as part of your application. You also learned how to compare objects so
that you can sort them in different ways.
As part of processing data, you learned how to store data using basic and advanced data
structures. Knowing how to efficiently process data is very important so that you avoid
resource contention scenarios such as running out of memory, or requiring too much
processing or time to execute the task at hand. A big part of processing data efficiently
is about picking the right data structures and algorithms for the right problems. All the
new tools that you have added to your belt will help you make the correct decisions
when building your Java applications.
In the next lesson, we will have a look at some advanced data structures.
Learning Objectives
Introduction
In the previous lessons, you learned about various data structures in Java, such as lists,
sets, and maps. You also learned about how to iterate on the many data structures,
compare objects in many different ways; and sort these collections in an efficient way.
In this lesson, you will learn the implementation details of advanced data structures
such as linked lists and binary search trees. As we progress, you'll also learn about a
powerful concept called enumerations and explore how to use them effectively instead
of constants. At the end of the lesson, you will gain an understanding of the magic and
mystery behind equals() and hashCode().
Disadvantages of ArrayList
Disadvantages of ArrayList are as follows:
• Though ArrayList is dynamic and the size need not be mentioned during creation.
However as the size of arrays is fixed, therefore ArrayLists often need to be
implicitly resized when more elements are added to the list. Resizing follows the
procedure of creating a new array and adding all the elements of the previous
array into a new array.
• Inserting a new element at the end of the ArrayList is often faster than adding in
between, however, it's expensive when elements are added in between the list,
because room has to be created for the new elements, and to create room existing
elements have to shift.
• Deleting the last element of the ArrayList is often faster, however, it's expensive
when elements are deleted in between, because the element has to be adjusted,
shifting elements to the left.
In this topic, you will learn how to build a custom linked list for specific purposes.
By doing this, we will appreciate the power of linked list and understand the
implementation details as well.
Here is a diagrammatic representation of a linked list:
next = null;
}
Node getNext() {
return next;
}
void setNext(Node node) {
next = node;
}
Object getData() {
return data;
}
}
3. Implement the add(Object item) method so that any item/object can be added
into this list. Construct a new Node object by passing the newItem = new Node(item)
item. Start with the head node, and move towards the end of the list, visiting each
node. In the last node, set the next node as our newly created node (newItem).
Increment the index by invoking incrementIndex() to keep track of the index:
// appends the specified element to the end of this list.
public void add(Integer element) {
// create a new node
Node newNode = new Node(element);
//if head node is empty, create a new node and assign it to Head
//increment index and return
if (head == null) {
head = newNode;
return;
}
4. Implement a toString() method to represent this object. Starting from the head
node, iterate all the nodes until the last node is found. On each iteration, construct
a string representation of an Integer stored in each node. The representation will
look similar to this: [Input1,Input2,Input3]
public String toString() {
String delim = ",";
StringBuffer stringBuf = new StringBuffer();
if (head == null)
return "LINKED LIST is empty";
7. Write a main method and add create an object of SimpleObjLinkedList and add five
Strings one after the other ("INPUT-1", "INPUT-2", "INPUT-3", "INPUT-4","INPUT-5")
into it respectively. Print the SimpleObjLinkedList object. In the main method,
get the item from the list using get(2) and print the value of the item retrieved,
also remove the item from the list remove(2) and print the value of the list. One
element should have been deleted from the list.
The output will be as follows:
[INPUT-1 ,INPUT-2 ,INPUT-3 ,INPUT-4 ,INPUT-5 ]
INPUT-3
[INPUT-1 ,INPUT-2 ,INPUT-3 ,INPUT-5 ]
Note
The solution for this activity can be found on page 356.
A BST is a special implementation of a binary tree, where the left-child node is always
less than or equal to the parent node, and the right-child node is always greater than or
equal to the parent node. This unique structure of the binary search tree makes it easier
to add, delete, and search for elements of the tree. The following diagram represents a
BST:
//constructor of Node
public Node(int data) {
this.data = data;
}
}
2. We will create a add(int data) function, which will check whether the parent node
is empty. If it is empty, it will add the value to the parent node. If the parent node
has data, we need to create a new Node(data) and find the right node (according to
the BST rule) to attach this new node.
To help find the right node, a method, add(Node root, Node newNode), has been
implemented to use the recursive logic to go deeper and find the actual node to
which this new node should belong.
As per BST rules, if the root data is greater than the newNode data, then newNode has
to be added to the left Node. Again, recursively check whether it has child nodes,
and the same logic of BST applies until it reaches the leaf node to add a value. If
the root data is less than the newNode data, newNode has to be added to the right
node. Again, recursively check whether it has child nodes, and the same logic of
BST applies until it reaches the leaf node to add a value:
/**
* This is the method exposed as public for adding elements into the Tree.
* it checks if the size == 0 and then adds the element into parent
node. if
* parent is already filled, creates a New Node with data and calls the
* add(parent, newNode) to find the right root and add it to it.
* @param data
*/
public void add(int data) {
if (size == 0) {
parent.data = data;
size++;
} else {
add(parent, new Node(data));
}
}
/**
* Takes two params, root node and newNode. As per BST, check if the root
* data is > newNode data if true: newNode has to be added in left Node
* (again recursively check if it has child nodes and the same logic of
BST
* until it reaches the leaf node to add value) else: newNode has to be
* added in right (again recursively check if it has child nodes and the
* same logic of BST until it reaches the leaf node to add value)
*
* @param root
* @param newNode
*/
private void add(Node root, Node newNode) {
if (root == null) {
return;
}
if (newNode.data < root.data) {
if (root.left == null) {
root.left = newNode;
size++;
} else {
add(root.left, newNode);
}
}
if ((newNode.data > root.data)) {
if (root.right == null) {
root.right = newNode;
size++;
} else {
add(root.right, newNode);
}
}
}
3. Create a traverseLeft() function to traverse and print all the values of the BST in
the left-hand side of the root node:
public void traverseLeft() {
Node current = parent;
System.out.print("Traverse the BST From Left : ");
while (current.left != null && current.right != null) {
System.out.print(current.data + "->[" + current.left.data + "
" + current.right.data + "] ");
current = current.left;
}
System.out.println("Done");
}
4. Create a traverseRight() function to traverse and print all the values of the BST on
the right-hand side of the root node:
public void traverseRight() {
Node current = parent;
System.out.print("Traverse the BST From Right");
while (current.left != null && current.right != null) {
System.out.print(current.data + "->[" + current.left.data + "
" + current.right.data + "] ");
current = current.right;
}
System.out.println("Done");
}
5. Let's create an example program to test the functionality of the BST:
/**
* Main program to demonstrate the BST functionality.
* - Adding nodes
* - finding High and low
* - Traversing left and right
* @param args
*/
public static void main(String args[]) {
bst.traverseRight();
}
}
The output is as follows:
Traverse the BST From Left : 32->[3 50] Done
Traverse the BST From Right32->[3 50] 50->[40 93] Done
Note
The solution for this activity can be found on page 360.
Enumerations
Enumeration in Java (or enum) is a special type in Java whose fields consist of constants.
It is used to impose compile-time safety.
For example, consider the days of the week, they are a set of fixed constants, therefore
we can have an enum defined:
public enum DayofWeek {
}
Now we can simply check if a variable that stores a day is part of the declared enum. We
can also declare enums for non-universal constants, such as:
public enum Jobs {
}
This will enforce the job-type to be the constants declared in the Jobs enum. Here's an
example enum holding currencies:
public enum Currency {
USD, INR, DIRHAM, DINAR, RIYAL, ASD
}
}
}
2. Let's create a enum holding directions with an integer value representing the
directions:
public enum Direction
{
EAST(45), WEST(90), NORTH(180), SOUTH(360);
int no;
Direction(int i){
no =i;
}
}
The output is as follows:
NORTH : 180
SOUTH : 360
4. Let's write a main method and sample program to demonstrate the use of enums:
Output will be as follows:
BACHELOR OF ENGINEERING : 1
BACHELOR OF ENGINEERING : 1
BACHELOR OF COMMERCE : 2
BACHELOR OF SCIENCE : 3
BACHELOR OF ARCHITECTURE : 4
BACHELOR : 0
true
Note
The solution for this activity can be found on page 362.
Note
The solution for this activity can be found on page 363.
• If none of the objects in the set match the hashCode of the added object, then we
can be 100% confident that no other object is available with the same identity.
A newly added object will be added safely to the set (without needing to check
equals()).
• If any of the objects match the hashCode of the object added, it means it might be
an identical object added (as hashCode may be the same for two different objects).
In this case, to confirm the suspicion, it will use the equals() method to see if the
objects are really equal. If equal, the newly added object will not be rejected, else
newly added objected will be rejected.
m.setYearOfPassing(2011);
//this element will not be added if hashCode and equals methods are
implemented
set.add(m3);
System.out.println("After Adding ALLEN for second time: ");
for (Student mm : set) {
System.out.println(mm.getName() + " " + mm.getAge());
}
}
}
Summary
In this lesson, we learned what a BST is and the steps to implement the basic
functionalities of a BST in Java. We also learned a technique to traverse a BST to
the right and left. We looked at the use of enums over constants and gained an
understanding of the types of problems they solve. We also built our own enums and
wrote code to fetch and compare the values of enums.
We also learned how HashSet is able to identify duplicates and looked at the significance
of overriding equals() and hashCode(). Also, we learned how to correctly implement
equals() and hashCode().
• Acquire and release resources in a way that respects exceptions without creating leaks
Introduction
Exception handling is a powerful mechanism for handling erroneous cases that occur
while our code is running. It enables us to focus on the main execution of the program
and separate the error-handling code from the expected execution path. The Java
language forces programmers to write exception-handling code for library methods,
and IDEs such as IntelliJ, Eclipse, and so on help us generate the boilerplate code
necessary. However, without proper guidance and understanding, standard exception
codes may result in more harm than good. This lesson is a practical introduction to
exceptions that will push you to contemplate various aspects of exception handling, and
will provide a number of rules of thumb that may be helpful when you are dealing with
exceptions in your programming life.
Here is a function in C that handles errors similar to our preceding pseudo code.
int other_idea()
{
int err = minor_func1();
if (!err)
err = minor_func2();
if (!err)
err = minor_func3();
return err;
}
When you code using primitive languages such as C, you inevitably feel strong
tension between readability and completeness. Luckily, in most modern programming
languages, we have exception handling capabilities that reduce this tension. Your code
can both be readable and can handle errors at the same time.
The main language construct behind exception handling is the try-catch block. The
code you put after the try is executed line by line. If any of the lines result in an error,
the rest of the lines in the try block are not executed and the execution goes to the
catch block, giving you a chance to handle the error gracefully. Here, you receive an
exception object that contains detailed information about the problem. However, if no
error happens in the try block, the catch block is never executed.
Here, we modify our initial example to handle errors using the try-catch block instead
of many if statements:
Try
Do step 1
Do step 2
Do step 3
Catch error
Handle error appropriately
Done
In this version, our code is placed between the try and catch keywords. Our code is
free from error-handling code that would otherwise prevent readability. The default
expected path of the code is quite clear: step 1, step 2, and step 3. However, if an error
happens, the execution moves to the catch block immediately. There, we receive
information about what the problem was in the form of an exception object and we are
given a chance to handle the error gracefully.
Most of the time, you will have code pieces that depend on one another. So, if an error
happens in one of the steps, you usually do not want to execute the rest of the steps,
since they depend on the success of that earlier step. You can use try-catch blocks
creatively to denote code dependencies. For example: in the following pseudo code,
there are errors in steps 2 and step 5. The steps that successfully get executed are steps
1 and 4. Since step 4 and later steps are independent of the success of the first three
steps, we were able to denote their dependencies with two separate try - catch blocks.
The error in step 2 prevented the execution of step 3, but not step 4:
Try
Do step 1
Do step 2 - ERROR
Do step 3
Catch error
Handle error appropriately
Done
Try
Do step 4
Do step 5 - ERROR
Do step 6
Catch error
Handle error appropriately
Done
If there is an exception and you do not catch it, the error will be propagated to the
caller. If this is your application, you should never let errors propagate out of your
code, to prevent your app from crashing. However, if you are developing a library that is
called by other code, letting errors propagate to the caller is sometimes a good idea. We
will discuss this in more detail later.
Note that results 2 and 5 contain division operations in which we divide a number by
zero, which results in an exception. This way, we are intentionally creating exceptions
in these two lines to see how execution progresses in the case of exceptions. Here is a
breakdown of the expected execution:
• Result 1 should print well.
• During result 2's execution we should get an exception, which should prevent
result 2 from printing.
• Because of the exception, execution should jump to the catch block, which should
prevent result 3 from printing.
• Result 4 should print well.
• Just like result 2, during result 5's execution we should get an exception, which
should prevent result 5 from printing.
• Similarly, because of the exception, the execution should jump to the catch block,
which should prevent result 6 from printing.
With the help of the two try-catch blocks, we should skip results 3 and 6 because of
the exceptions on results 2 and 5. This should leave only results 1 and 4, which will be
executed successfully.
This shows that our preceding discussion was correct. Also, to verify the execution
order, place a breakpoint in the result 1 line and click step over to watch how execution
progresses step by step with the try - catch block.
With the help of exceptions and the try-catch block, we are able to write code that
focuses more on the expected default execution path, while ensuring that we handle
unexpected error cases and can recover or fail gracefully, depending on the severity of
the error.
So, as a newbie Java developer, all you wanted was to call a method and now you are
forced to do something about an exception that it may throw. Your IDE can generate
code that takes care of the exception. However, default generated code is usually
not the best. A newbie with no guidance and the powers of IDE code generation can
create code that is quite bad. In this section, you will be guided on how best to use
IDE-generated exception-handling code.
Let's say you wrote the following code to open and read a file:
import java.io.File;
import java.io.FileInputStream;
Currently, your code will not compile and your IDE underlined the FileInputStream
constructor in red. This is because it may throw an exception, as specified in its source
code:
public FileInputStream(File file) throws FileNotFoundException {
At this point, your IDE usually tries to be helpful. When you move the caret on to
FileInputStream and hit Alt + Enter in IntelliJ, for example, you will see two quick-fix
options: Add exception to method signature and Surround with try/catch. These
correspond to the two options you have when dealing with specified exceptions, which
we will learn about later in more depth. Here's what the first option converts your code
into:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
}
}
Now your main function also specifies that it can throw an exception. Such an
exception causes the program to exit immediately, which may or may not be what you
want. If this was a function that you give others as a library, this change would prevent
their code from compiling, unless they, in turn, did something about the specified
exception, just like you. Again, this may or may not be what you want to do.
If you selected "Surround with try/catch", which was the second option that IntelliJ
provided, here is what your code would become:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
If we cannot find this file to open, we should think cleverly about what we can do.
Should we ask the user to look for the file? Should we download it from the internet?
Whatever we do, taking a note in an obscure log file and sweeping the problem under
the rug is probably one of the worst ways to handle the problem. If we cannot do
anything useful, maybe not handling the exception and letting our caller deal with it
would be a more honest way of dealing with the problem.
Notice that there is no silver bullet, or one-size-fits-all suggestion here. Every
exceptional case, every application, every context, and every user base is different, and
we should come up with an exception handling strategy that fits the current situation
best. However, if all you are doing is e.printStackTrace(), you are probably doing
something wrong.
5. Go to the first issue (FileInputStream), press Alt + Enter, and select "Add exception
to method signature". This is how your code should look now:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
int data = 0;
while(data != -1) {
data = fileInputStream.read();
System.out.println(data);
}
fileInputStream.close();
}
}
We specified that our main function can throw FileNotFoundException, but this was
not enough as this is not the exception type that the other functions throw. Now
go to the first remaining issue (read), press Alt + Enter, and select "Add exception
to method signature" once again. This is how your code should look now:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
int data = 0;
while(data != -1) {
data = fileInputStream.read();
System.out.println(data);
}
fileInputStream.close();
}
}
Now let's run our code. Unless you created an input.txt in the meantime, this is
what you should see as an output:
Exception in thread "main" java.io.FileNotFoundException: input.txt (The
system cannot find the file specified)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at Main.main(Main.java:9)
The exception propagated out of our main function and the JVM caught it and
logged into the console.
Two things happened here. First, fixing the problem for read() was enough to
eliminate all problems from the code, since both read and close throw the same
exception: IOException, which is listed in the throws statement in the main
function's declaration. However, the FileNotFoundException exception that we had
listed there disappeared. Why?
This is because exception classes are in a hierarchy and IOException is an ancestor
class of FileNotFoundException. Since every FileNotFoundException is also an
IOException, specifying IOException was enough. If these two classes were not
related in that way, IntelliJ would list the possible thrown exceptions as a comma-
separated list.
6. Now let's provide the input.txt to our program. You can create the input.txt
anywhere in your hard drive and provide a full path in the code; however, we will
use a simple approach: IntelliJ runs your program inside the main project folder.
Right-click on your project's src folder and click Show in Explorer. Now you
should see the contents of the folder that contains the src folder; this is the root
of your project folder. Create an input.txt file here and write the text "abc" in it. If
you run your program again, you should see an output similar to this:
97
98
99
-1
7. Specifying the exceptions was one way to make our program work. Another would
be to catch them. Let's try that now. Go back to the following version of your file;
you can use undo repeatedly to do that:
import java.io.File;
import java.io.FileInputStream;
int data = 0;
while(data != -1) {
data = fileInputStream.read();
System.out.println(data);
}
fileInputStream.close();
}
}
8. Now move the caret on to FileInputStream, hit Alt + Enter, and select "Surround
with try/catch". Here is how your code should look:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
int data = 0;
while(data != -1) {
data = fileInputStream.read();
System.out.println(data);
}
fileInputStream.close();
}
}
Notice what happened here. Instead of simply wrapping the line with a try/
catch block, it actually separated the creation of the reference variable from the
exception-generating constructor call. This is mainly because fileInputStream is
used later in the code and moving it inside the try/catch block would prevent it
from being visible to those usages. This is actually a common pattern; you declare
the variable before the try/catch block, handle any issues with its creation, and
make it available for later, if necessary.
9. The current code has a problem: if the FileInputStream inside the try/catch block
fails, the fileInputStream will continue to be null. After the try/catch block, it will
be dereferenced and you will get a null reference exception. You have two options:
either you place all usages of the object in the try/catch block, or you check the
reference for null. Here is the first of the two options:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
int data = 0;
while(data != -1) {
data = fileInputStream.read();
System.out.println(data);
}
fileInputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
10. We moved the code inside the try/ catch block to make sure we don't dereference
fileInputStream while null. However, we still have red underlines under read() and
close(). Alt + Enter on read() gives you a couple of options, the first of which is to
add a catch clause:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
11. Here is the second of the two options, not placing all the code inside the first try:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
if (fileInputStream != null) {
int data = 0;
while(data != -1) {
try {
data = fileInputStream.read();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(data);
}
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
It is not good practice to use code like this. Although the quick fixes with Alt +
Enter usually serve us quite well, in this example, they resulted in horrible code.
This code here implies that your stream may sometimes fail. In that case, those
failures should be ignored and we should keep trying to read from the stream.
Also, the stream may fail to close, which we should also ignore. This would be a
very rare scenario and this code is not good. It's not readable at all either, with
many try/catch blocks.
13. A better way would be to place the whole block in a try/catch. In this case, we
are giving up after the first error, which is a simpler and usually more correct
approach:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
if (fileInputStream != null) {
try {
int data = 0;
while(data != -1) {
data = fileInputStream.read();
System.out.println(data);
}
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
To create this code, we did not rely on IntelliJ's quick fix with Alt + Enter. Since it's quite
good usually, you may think that the code it creates is correct. However, you have to use
your judgement, and sometimes correct the code it creates, as in this example.
Now you have experienced the quick and dirty handling of exceptions using the help
of an IDE. The skills you gained in this section should guide you when you are on a
deadline and help you avoid pitfalls when using autogenerated exception code using an
IDE.
There are a number of drawbacks to the method of handling errors used here. In this
code, all we are trying to do is call three functions. However, for each function call,
we are passing around values to track error states and using if statements for each
function call if there was an error. Furthermore, the return value of the function is the
error state—you are not allowed to return a value of your choosing. All this extra work
dilutes the original code and makes it difficult to understand and maintain.
Another limitation of this approach is that a single integer value may not represent the
error sufficiently. Instead, we may want to have more details about the error, when it
happened, about which resource, and so on.
Before exception handling, this was how programmers had to code to ensure the
completeness of their programs. Exception handling brings a number of benefits.
Consider this alternate Java code:
int otherIdea() {
try {
minorFunc1();
minorFunc2();
minorFunc3();
} catch (IOException e) {
// handle IOException
} catch (NullPointerException e) {
// handle NullPointerException
}
}
Here, we have the three function calls without any error-related code polluting them.
These are placed in a try/catch block and error handling is done separately from the
original code in the catch blocks. This is more desirable for the following reasons:
• We do not have to have an if statement for each function call. We can group the
exception handling in one place. It does not matter which function raised the
exception; we catch all of them in one single place.
• There is not only one kind of problem that can happen in a function. Each function
can raise more than one kind of exception. These can be handled in separate catch
blocks, whereas, without exception handling, this would have required multiple if
statements per function.
• The exception is represented by an object, not a single integer value. While an
integer can tell us which kind of problem it was, an object can tell us much more:
the call stack at the time of exception, the related resource, the user-readable
explanation about the problem, and so on, can all be provided along with the
exception object. This makes it much easier to act appropriately to exceptions
compared to a single integer value.
int r = rand.nextInt(10);
if (r < 2) {
return EC_IO;
}
if (r > 8) {
return EC_INTERRUPTION;
}
System.out.println("ecFunction2 done");
return EC_NONE;
}
6. Create callThrowingFunctions() as follows:
private void callThrowingFunctions() {
try {
thFunction1();
thFunction2();
} catch (IOException e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
} catch (InterruptedException e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
}
7. Create a method called callErrorCodeFunctions() as follows:
private void callErrorCodeFunctions() {
int err = ecFunction1();
if (err != EC_NONE) {
if (err == EC_IO) {
System.out.println("An I/O exception occurred in ecFunction1.");
}
}
err = ecFunction2();
switch (err) {
case EC_IO:
System.out.println("An I/O exception occurred in ecFunction2.");
break;
case EC_INTERRUPTION:
Furthermore, since the error is represented simply by a number, we are not able to get
detailed information in the error-handling code. We also have to have error-handling
code for each function call, as we would not have a way of differentiating between error
locations otherwise. This creates code that is much more complicated and verbose than
it should be.
Play with the code further, run it many times, and observe its behavior. This should give
you a better understanding of exceptions versus error codes and why exceptions are
superior.
Note
The solution for this activity can be found on page 365.
Exception Sources
When an exceptional case occurs in code, an exception object is thrown by the source
of the problem, which is in turn caught by one of the callers in the call stack. The
exception object is an instance of one of the exception classes. There are many such
classes, which represent various types of problems. In this topic, we will take a look
at different types of exceptions, get to know some of the exception classes from Java
libraries, learn how to create our own exceptions, and see how to throw them.
In the previous topic, we first played with IOException. Then, in the activity, we played
with NumberFormatException. There was a difference between these two exceptions. The
IDE would force us to handle IOException and would not compile our code otherwise.
However, it did not care whether we caught NumberFormatException or not, it would
still compile and run our code. The difference was in the class hierarchy. While both of
them are descendants of the Exception class, NumberFormatException is a descendant of
RuntimeException, a subclass of Exception:
The preceding figure shows a simple class hierarchy. Any class that is a descendant of
Throwable can be thrown and caught as an exception. However, Java provides a special
treatment for the descendants of Error and RuntimeException classes. We'll explore
these further in the upcoming sections.
Checked Exceptions
Any descendant of Throwable that is not a descendant of Error or RuntimeException falls
in the category of checked exceptions. For example: IOException, which we used in the
previous topic, is a checked exception. The IDE forced us to either catch it or to specify
that we throw it in our function.
To be able to throw a caught exception, your function has to specify that it throws the
exception.
Here, we created a function and wanted it to throw an IOException. However, our IDE
will not let us do that because this is a checked exception. Here is the type hierarchy of
it:
Notice that our code still has a problem. We will continue dealing with it in the next
exercise.
Another requirement of checked exceptions is that if you call a method that specifies
a checked exception, you have to either catch the exception or specify that you also
throw that exception. This is also known as "the catch or specify rule."
e.printStackTrace();
}
}
}
While this compiles and runs, remember that simply printing information about it
is not the greatest way to handle an exception.
In these exercises, we saw how to throw checked exceptions and how to call methods
that throw them.
Unchecked Exceptions
Recall the top of the exception class hierarchy:
Here, the exception classes that are descendants of RuntimeException are called
runtime exceptions. The descendants of Error are called errors. Both of these are called
unchecked exceptions. They do not need to be specified, and if they are specified, they
do not need to be caught.
Unchecked exceptions represent things that may happen more unexpectedly compared
to checked exceptions. The assumption is that you have the option to ensure that they
will not be thrown; therefore, they do not have to be expected. However, you should do
your best to handle them if you have a suspicion that they may be thrown.
Note that this code is trying to parse a string as an integer, but the string clearly
does not contain an integer. As a result, a NumberFormatException will be thrown.
However, since this is an unchecked exception, we do not have to catch or specify
it. This is what we see when we run the code:
Exception in thread "main" java.lang.NumberFormatException: For input
string: "this is not a number"
at java.lang.NumberFormatException.
forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at Main.main(Main.java:6)
2. Since we did not catch it, the NumberFormatException got thrown from the main
function and crashed the application. Instead, we could catch it and print a
message about it as follows:
public class Main {
public static void main(String[] args) {
try {
int i = Integer.parseInt("this is not a number");
} catch (NumberFormatException e) {
System.out.println("Sorry, the string does not contain an integer.");
}
}
}
Now, when we run the code, we get an output that shows that we are aware of the
situation:
Sorry, the string does not contain an integer.
Although catching unchecked exceptions is optional, you should make sure you catch
them in order to create code that is complete.
It's practically the same case for errors, which are descendants of the Error class. In the
following section, we talk about the semantic differences between runtime exceptions
and errors.
Beyond the mechanics of exception handling, the choice of exception class also
carries semantic information. For example: if a library method encounters a case
in which a file that was supposed to be in the hard drive is missing, it would throw
an instance of FileNotFoundException. If there was a problem in a string that was
supposed to contain a numeric value, the method that you give that string to would
throw a NumberFormatException. The Java class library contains a number of exception
classes that fit most unexpected situations. The following is a subset of classes in this
hierarchy:
If you read through this list, you will notice that there are a lot of exception types for
various occasions.
Now move the caret over Throwable and press Ctrl + H. The hierarchy window should
open with the Throwable class in focus. It should look like this:
Now expand Error and Exception, and read through the list of classes. These are various
throwable classes defined in various libraries that your code has access to. As you can
see, there is quite a broad list of exceptions to choose from. Next to each exception
class, there is the package that it belongs to in parentheses. As a rule of thumb, if you
are going to throw an exception yourself, you should try to use exceptions that are in
the libraries that you are also using. For example: importing com.sun.jmx.snmp.IPAcl just
so that you can use the ParseException defined in it is not a good thing to do.
Now you have a better idea about the existing exception classes that are in the Java
Class Library and what your choice of exception class communicates to the users of
your code.
if (digitString.length() > 1) {
throw new NumberFormatException("Please supply a string with a
single digit");
}
System.out.println(digitString);
}
try {
useDigitString("5");
useDigitString("");
useDigitString("7");
} catch (NumberFormatException e) {
System.out.println("A number format problem occured: " +
e.getMessage());
}
}
Notice that, from main, we call runDigits, which in turn calls useDigitString.
The main function catches IllegalArgumentException and runDigits catches
NumberFormatException. Although we throw all the exceptions in useDigitString, they
are caught in different places.
if (digitString.length() > 1) {
throw new NumberFormatException("Please supply a string with a
single digit");
}
System.out.println(digitString);
}
try {
useDigitString("5");
useDigitString("");
useDigitString("7");
} catch (NumberFormatException e) {
System.out.println("A number format problem occured: " +
e.getMessage());
}
}
3. Add the main() method as follows:
public static void main(String[] args) {
try {
runDigits();
} catch (EmptyInputException e) {
System.out.println("An empty string was provided");
}
}
Notice that this made our code much simpler—we did not even have to write a
message, as the name of the exception clearly communicates the problem. Here is
the output:
1
A number format problem occured: Please supply a string with a single
digit
5
An empty string was provided
Now you know how to throw exceptions and create your own exception class if existing
exception classes are insufficient.
Note
The solution for this activity can be found on page 366.
Exception Mechanics
In the previous topics, we threw and caught exceptions and got a feel for how
exceptions work. Now let's revisit the mechanics to make sure we got everything right.
// line5, skipped
} catch (Exception e) {
// comes here after line3
}
The catch block catches throwables if they can be assigned to the exception reference
it contains (Exception e, in this case). So, if you have an exception class here that is high
up in the exception hierarchy (such as Exception), it will catch all exceptions. This will
not catch errors, which is usually what you want.
If you want to be more specific about the types of exceptions that you catch, you can
provide an exception class that is lower in the hierarchy.
}
Note that this code will not even compile. The code throws an exception, but
the catch clause expects an InstantiationException, which is a descendant of
Exception, to which exception instances cannot be assigned. Therefore, the
exception is neither caught, nor thrown.
}
When we run the code, we see that we are not able to catch the exception that we
threw:
line 0
line 1
line 2
line 3
Exception in thread "main" java.lang.Exception: EXCEPTION!
at Main.main(Main.java:8)
Sometimes, you catch one type of a specific exception, but your code can throw other
types of exceptions as well. You can provide multiple catch blocks in this case. The
exception types being caught can be in different places in the class hierarchy. The first
catch block of whose parameter the thrown exception can be assigned to is executed.
So, if two exception classes have an ancestor relationship, the descendant's catch
clause has to go before the ancestor's catch clause; otherwise, the ancestor would catch
the descendant's exceptions as well.
}
3. If the thrown exception is an InstantiationException, it will be caught by the first
catch. Otherwise, if it is any other exception, it will be caught by the second. Let's
try reordering the catch blocks:
public class Main {
public static void main(String[] args) {
try {
for (int i = 0; i < 5; i++) {
System.out.println("line " + i);
if (i == 3) throw new Exception("EXCEPTION!");
}
} catch (Exception e) {
e.printStackTrace();
} catch (InstantiationException e) {
System.out.println("Caught an InstantiationException");
}
}
Now our code will not even compile because instances of InstantiationException
can be assigned to Exception e, and they will be caught by the first catch block.
The second block will never be called—ever. The IDE is smart to catch this problem
for us.
Another property of exceptions is that they travel up the call stack. Every function that
is called essentially returns the execution to its caller, until one of them is able to catch
the exception.
method2();
} catch (Exception e) {
System.out.println("method1 caught an Exception!: " +
e.getMessage());
System.out.println("Also, below is the stack trace:");
e.printStackTrace();
}
System.out.println("End method 1");
}
2. Add the main() method as follows:
public static void main(String[] args) {
System.out.println("Begin main");
method1();
System.out.println("End main");
}
}
When we run the code, we get this output:
Begin main
Begin method 1
Begin method 2
Begin method 3
line 0
line 1
line 2
line 3
method1 caught an Exception!: EXCEPTION!
Also, below is the stack trace:
java.lang.Exception: EXCEPTION!
at Main.method3(Main.java:8)
at Main.method2(Main.java:18)
at Main.method1(Main.java:25)
at Main.main(Main.java:36)
End method 1
End main
Notice that method 2 and method 3 do not run to completion, while method 1 and
main do. Method 2 throws the exception; method 3 does not catch it and lets it
propagate up. Finally, method 1 catches it. Method 2 and method 3 abruptly return
the execution to the method higher in the call stack. Since method 1 and main do
not let an exception propagate up, they are able to run to completion.
There is one more feature of the catch block that we should talk about. Let's say we
would like to catch two specific exceptions but not others, but we will do the exact
same thing in their catch blocks. In this case, we are able to combine the catch blocks
of these exceptions with a pipe character. This feature was introduced in Java 7 and will
not work in Java 6 and below.
7. Get the user's height. If it is lower than 130, throw a TooShortException with this
name and age.
8. Print the name as "John is riding the roller coaster."
9. Catch the two types of exceptions separately. Print appropriate messages for each.
10. Run the main program.
The output should be similar to the following:
Enter name of visitor: John
Enter John's age: 20
Enter John's height: 180
John is riding the roller coaster.
Enter name of visitor: Jack
Enter Jack's age: 13
Jack is 13 years old, which is too young to ride.
Enter name of visitor: Jill
Enter Jill's age: 16
Enter Jill's height: 120
Jill is 120 cm tall, which is too short to ride.
Enter name of visitor:
Note
The solution for this activity can be found on page 368.
3. You may try to devise various solutions to this resource-leaking problem. For
example: you may duplicate the file-closing code and paste it to the catch block.
Now you have it both in the try and in the catch blocks. If you have multiple catch
blocks, all of them should have it as follows:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main {
private static void useTheFile(String s) {
System.out.println(s);
throw new RuntimeException("oops");
}
public static void main(String[] args) throws Exception {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("input.txt"));
System.out.println("opened the file");
useTheFile(br.readLine());
br.close();
System.out.println("closed the file");
} catch (IOException e) {
System.out.println("caught an I/O exception while reading the file");
br.close();
System.out.println("closed the file");
} catch (Exception e) {
System.out.println("caught an exception while reading the file");
br.close();
System.out.println("closed the file");
}
}
}
4. The preceding code is correct, but it has code duplication, which makes it hard to
maintain. Instead, you may think that you can close the file after the catch block in
one single place:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main {
private static void useTheFile(String s) {
System.out.println(s);
} catch (IOException e) {
System.out.println("caught an I/O exception while reading the file");
throw new Exception("something is wrong with I/O", e);
} catch (Exception e) {
System.out.println("caught an exception while reading the file");
} finally {
br.close();
System.out.println("closed the file");
}
}
}
This new version closes the file whether there was an exception raised or not, or
if another exception was raised after an exception was originally caught. In each
case, the file-closing code in the finally block is executed and the file resource is
released by the operating system appropriately.
There is still one problem with this code. The problem is, an exception might be
raised while we are opening the file in the BufferedReader constructor, and the br
variable may remain null. Then, when we try to close the file, we will dereference a
null variable, which will create an exception.
6. To avoid this problem, we need to ignore br if it is null. The following is the
complete code:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("input.txt"));
System.out.println("opened the file");
useTheFile(br.readLine());
} catch (IOException e) {
Note
The solution for this activity can be found on page 370.
For this to work, all of these resources have to implement the AutoCloseable interface.
2. Create a Main class with the useTheFile() method, which takes a string parameter
as follows:
public class Main {
Best Practices
While learning about exception handling and its statements, mechanics, and classes is
required to use it, for most programmers this may not be enough. Usually, this set of
theoretical information needs practical experience of various cases to get a better feel
for exceptions. In this regard, some rules of thumb about the practical use of exceptions
are worth mentioning:
• Do not suppress exceptions unless you really handled them.
• Inform the user and let them take responsibility unless you can fix things silently.
• Be aware of the caller's behavior and don't leak exceptions unless it is expected.
• Wrap and chain with more specific exceptions when possible.
Suppressing Exceptions
In your function, when you catch an exception and do no throw anything, you are
signaling that you took care of the exceptional case and you fixed the situation so that it
is as if that exceptional case had never happened. If you cannot make such a claim, then
you should not have suppressed that exception.
int sum = 0;
for(Integer i: outputList) {
sum += i;
}
System.out.println("Sum is " + sum);
}
int sum = 0;
for(Integer i: outputList) {
sum += i;
}
System.out.println("Sum is " + sum);
}
}
Now here is our output:
could not parse an element: two
Sum is 4
It added 1 and 3 together, and ignored the "two." Is this what we wanted? We assumed
that the "two" was the correct number and we expected it to be in the sum. However, at
the moment, we are excluding it from the sum, and we are adding a note in the logs. If
this was a real-life scenario, probably nobody would look at the logs and the result that
we provide would be inaccurate. This is because we caught the error and did not do
anything meaningful about it.
What would be a better approach? We have two possibilities here: we either can assume
that every element in the list should actually be a number, or we can assume that there
will be mistakes and we should do something about them.
The latter is a trickier approach. Perhaps we can collect the offending entries in another
list and return it back to the caller, and then the caller would send it back to its origin
for re-evaluation. For example, it could show them to the user and ask them to be
corrected.
The former is an easier approach: we assume that the initial list contains number
strings. If this assumption breaks, however, we have to let the caller know. So, we
should throw an exception, rather than providing a half-correct sum.
What we should not do is take a third approach: hope that the list contains numbers,
but ignore the ones that are not numbers. Note that this is a choice we made, but
that's not what we thought of when we enumerated our two options above. This was
convenient to program, but it created an assumption that was not there in the original
business logic. Be very careful about situations like this. Make sure you write down
your assumptions and be strict in enforcing them. Do not let the convenience of
programming force you to accept weird assumptions.
If we take the assumption that the initial list contains number strings, here is how we
should have coded it:
import java.util.ArrayList;
import java.util.List;
try {
List<Integer> outputList = parseIntegers(inputList);
int sum = 0;
for(Integer i: outputList) {
sum += i;
}
System.out.println("Sum is " + sum);
} catch (NumberFormatException e) {
System.out.println("There was a non-number element in the list.
Rejecting.");
}
}
int sum = 0;
for(Integer i: outputList) {
sum += i;
}
System.out.println("Sum is " + sum);
done = true;
} catch (NonNumberInListException e) {
System.out.println("This element does not seem to be a
number: " + inputList.get(e.index));
System.out.print("Please provide a number instead: ");
Scanner scanner = new Scanner(System.in);
String newValue = scanner.nextLine();
inputList.set(e.index, newValue);
}
}
}
}
And here is a sample output:
This element does not seem to be a number: two
Please provide a number instead: 2
Sum is 6
Note that we identified the offending element and asked the user to fix it. This is a good
way to keep the user in the loop and give them a chance to fix the problem.
Consider Chaining and Being More Specific When You Let Exceptions
Propagate
When you propagate an exception to your caller, you usually have a chance to add
more information to that exception so that it will be more useful to the caller. For
example: you may be parsing the user's age, phone number, height, and so on, from
strings that they provided. Simply raising a NumberFormatException, without informing
the caller about which value it was for is not a very helpful strategy. Instead, catching
the NumberFormatException separately for each parse operation gives us the chance
to identify the offending value. Then, we can create a new Exception object, provide
more information in it, give the NumberFormatException as the initial cause, and throw
that instead. Then, the caller can catch it and be informed about which entity was the
offending one.
The earlier exercise, in which we used our custom NonNumberInListException to identify
the index of the offending entry in the list, is a good example of this rule of thumb.
Whenever possible, it is a better idea to throw a more informative exception that we
create ourselves, rather than letting the internal exception propagate without much
context.
Summary
In this lesson, we covered exceptions in Java from a practical point of view. First, we
discussed the motivation behind exception handling and how it provides advantages
over other ways of trying to handle erroneous cases. Then, we took the point of view
of a newbie Java programmer with a powerful IDE and provided guidance on how to
best handle and specify exceptions. Later, we dived deeper into causes of exceptions
and various exception types, followed by the mechanics of exception handling using
the try/catch, try/catch/finally, and try with resource blocks. We finish this discussion
with a list of best practices to guide your decision process in various situations that
involve exceptions.
This section is included to assist the students to perform the activities in the book.
It includes detailed steps that are to be performed by the students to achieve the objectives of
the activities.
Activity 2: Reading Values from the User and Performing Operations Using
the Scanner Class.
Solution:
1. Right-click the src folder and select New | Class.
2. Enter ReadScanner as the class name, and then click OK.
3. Import the java.util.Scanner package:
import java.util.Scanner;
4. In the main() enter the following:
public class ReadScanner
{
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
System.out.print("Enter a number: ");
int a = sc.nextInt();
System.out.print("Enter 2nd number: ");
int b = sc.nextInt();
}
}
5. Run the main program.
The output will be as follows:
Enter a number: 12
Enter 2nd number: 23
The sum is 35.
default:
weatherWarning = "The weather looks good. Take a walk
outside";
break;
}
System.out.println(weatherWarning);
}
}
5. If the logic in step 3 is false, then print an appropriate message and break out of
the loop:
else
{
System.out.println("Sorry your request could not be processed");
break;
}
}
}
}
numberOfBoxesShipped += 1;
System.out.printf("%d boxes shipped, %d peaches remaining\n",
numberOfBoxesShipped, numberOfPeaches);
}
}
}
}
}
4. Inside the curly braces, create the following instance variables to hold our data, as
shown here:
public class Animal {
int legs;
int ears;
int eyes;
String family;
String name;
}
5. Below the instance variables, define two constructors. One will take no arguments
and initialize legs to 4, ears to 2, and eyes to 2. The second constructor will take
the value of legs, ears, and eyes as arguments and set those values:
public class Animal {
int legs;
int ears;
int eyes;
String family;
String name;
public Animal(){
this(4, 2,2);
}
public Animal(int legs, int ears, int eyes){
this.legs = legs;
this.ears = ears;
this.eyes = ears;
}
}
6. Define four methods, two to set and get the family and two to set and get the
name:
Note
The methods that set values in an object are called setters, while those that get
those values are called getters.
public Animal(){
this(4, 2,2);
}
public Animal(int legs, int ears, int eyes){
this.legs = legs;
this.ears = ears;
this.eyes = ears;
}
public String getFamily() {
return family;
}
We have finished constructing our Animal class. Let's continue and create a few
instances of this class.
7. Create a new file named Animals.java and copy the following code into it, as
shown here:
public class Animals {
}
}
8. Create two objects of the Animal class:
public class Animals {
public static void main(String[] args){
Animal cow = new Animal();
Animal goat = new Animal();
}
}
9. Let's create another animal with 2 legs, 2 ears and 2 eyes:
Animal duck = new Animal(2, 2, 2);
10. To set the animals' names and family, we will use the getters and setters we
created in the class. Copy/write the following lines into the Animals class:
public class Animals {
cow.setName("Cow");
cow.setFamily("Bovidae");
goat.setName("Goat");
goat.setFamily("Bovidae");
duck.setName("Duck");
duck.setFamily("Anatidae");
System.out.println(cow.getName());
System.out.println(goat.getName());
System.out.println(duck.getFamily());
}
}
The output of the preceding code is as follows:
Cow
Goat
Anatide
public Operator() {
this("+");
}
2. Create another class named Subtraction. It extends from Operator and override
the operate method with each operation that it represents. It also need a no-argu-
ment constructor that calls super passing the operator that it
represents:
public class Subtraction extends Operator {
public Subtraction() {
super("-");
}
@Override
public double operate(double operand1, double operand2) {
return operand1 - operand2;
}
}
3. Create another class named Multiplication. It extends from Operator and over-
ride the operate method with each operation that it represents. It also need a
no-argument constructor that calls super passing the operator that it represents:
public class Multiplication extends Operator {
public Multiplication() {
super("x");
}
@Override
public double operate(double operand1, double operand2) {
return operand1 * operand2;
}
}
4. Create another class named Division. It extends from Operator and override the
operate method with each operation that it represents. It also need a no-argument
constructor that calls super passing the operator that it represents:
public class Division extends Operator {
public Division() {
super("/");
@Override
public double operate(double operand1, double operand2) {
return operand1 / operand2;
}
}
5. As the previous Calculator class, this one will also have an operate method, but it
will only delegate to the operator instance. Last, write a main method that calls the
new calculator a few times, printing the results of the operation for each time:
public class CalculatorWithFixedOperators {
public static void main (String [] args) {
System.out.println("1 + 1 = " + new
CalculatorWithFixedOperators(1, 1, "+").operate());
System.out.println("4 - 2 = " + new
CalculatorWithFixedOperators(4, 2, "-").operate());
System.out.println("1 x 2 = " + new
CalculatorWithFixedOperators(1, 2, "x").operate());
System.out.println("10 / 2 = " + new
CalculatorWithFixedOperators(10, 2, "/").operate());
}
if (subtraction.matches(operator)) {
this.operator = subtraction;
} else if (multiplication.matches(operator)) {
this.operator = multiplication;
} else if (division.matches(operator)) {
this.operator = division;
} else {
this.operator = sum;
}
}
myCat.ears = 2;
myCat.legs = 4;
myCat.eyes = 2;
System.out.println(myCat.getFamily());
System.out.println(myCat.getName());
System.out.println(myCat.ears);
System.out.println(myCat.legs);
System.out.println(myCat.eyes);
}
}
The output is as follows
Cat
Puppy
2
4
2
@Override
public void makeSound() {
this.sound = "Moo";
this.onAnimalMadeSound();
}
6. Create another interface called AnimalListener with the following methods:
public interface AnimalListener {
void onAnimalMoved();
void onAnimalMadeSound();
}
7. Let the Cow class also implement this interface. Make sure that you override the
two methods in the interface.
8. Edit the two methods to look like this:
@Override
public void onAnimalMoved() {
System.out.println("Animal moved: " + this.movementType);
}
@Override
public void onAnimalMadeSound() {
System.out.println("Sound made: " + this.sound);
}
9. Finally, create a main method to test your code:
public static void main(String[] args){
Cow myCow = new Cow();
myCow.move();
myCow.makeSound();
}
}
10. Run the Cow class and view the output. It should look something like this:
Animal moved: Walking
Sound made: Moo
5. Create another file with SalesWithCommission class that extends Sales. Add a
constructor that receives the gross sales as double and store it as a field. Also add
a method called getCommission which returns a double that is the gross sales times
15% (0.15):
public class SalesWithCommission extends Sales implements Employee {
}
}
3. Inside the main method, create two variables:
Cat cat = new Cat();
Cow cow = new Cow();
4. Print the owner of the cat:
System.out.println(cat.owner);
5. Upcast the cat to Animal and try to print the owner once more. What error do you
get? Why?
Animal animal = (Animal)cat;
System.out.println(animal.owner);
The error message is as follows:
Figure 5.7: Exception while accessing the variables of the subclass for upcasting
Reason: Since we did an upcast, we cannot access the variables of the subclass
anymore.
6. Print the sound of the cow:
System.out.println(cow.sound);
7. Try to upcast the cow to Animal. Why error do you get? Why?
Animal animal1 = (Animal)cow;
The error message is as follows:
Reason: Cow does not inherit from the Animal class, so they don't share the same
hierarchical tree.
8. Downcast the animal to cat1 and print the owner again:
Cat cat1 = (Cat)animal;
System.out.println(cat1.owner);
9. The full AnimalTest class should look like this:
public class AnimalTest {
System.out.println(cat.owner);
}
3. Create an abstract method that returns the type of person in the hospital. Name
this method String getPersonType(), returning a String:
public abstract String getPersonType();
We have finished our abstract class and method. Now, we will continue to inherit
from it and implement this abstract method.
4. Create a new class called Doctor that inherits from the Person class:
public class Doctor extends Patient {
}
5. Override the getPersonType abstract method in our Doctor class. Return the string
"Arzt". This is German for Doctor:
@Override
public String getPersonType() {
return "Arzt";
}
6. Create another class called Patient to represent the patients in the hospital. Simi-
larly, make sure that the class inherits from Person and overrides the getPerson-
Type method. Return "Kranke". This is German for Patient:
public class People extends Patient{
@Override
public String getPersonType() {
return "Kranke";
}
}
Now, we have the two classes. We will now test our code using a third test class.
7. Create a third class called HospitalTest. We will use this class to test the two
classes we created previously.
8. Inside the HospitalTest class, create the main method:
public class HospitalTest {
public static void main(String[] args){
}
}
9. Inside the main method, create an instance of Doctor and another instance of
Patient:
Doctor doctor = new Doctor();
People people = new People();
10. Try calling the getPersonType method for each of the objects and print it out to the
console. What is the output?
String str = doctor.getPersonType();
String str1 = patient.getPersonType();
System.out.println(str);
System.out.println(str1);
The output is as follows:
@Override
public double getNetSalary() {
return grossSalary - getTax();
}
}
2. Create a new generic version of each type of employee: GenericEngineer. It will
need a constructor that receives gross salary and pass it to the super constructor.
It also needs to implement the getTax() method, returning the correct tax value
for each class:
public class GenericEngineer extends GenericEmployee {
@Override
public double getTax() {
}
3. Create a new generic version of each type of employee: GenericManager. It will
need a constructor that receives gross salary and pass it to the super constructor.
It also needs to implement the getTax() method, returning the correct tax value
for each class:
public class GenericManager extends GenericEmployee {
@Override
public double getTax() {
return (28.0/100) * getGrossSalary();
}
}
4. Create a new generic version of each type of employee: GenericSales. It will need a
constructor that receives gross salary and pass it to the super constructor. It also
needs to implement the getTax() method, returning the correct tax value for each
class:
public class GenericSales extends GenericEmployee {
@Override
public double getTax() {
return (19.0/100) * getGrossSalary();
}
@Override
public double getTax() {
return (19.0/100) * getGrossSalary();
}
}
6. Add a new method getEmployeeWithSalary to your EmployeeLoader class. This
method will generate a random salary between 70,000 and 120,000 and assign to
the newly created employee before returning it. Remember to also provide a gross
sales when creating a GenericSalesWithCommission employee:
public static Employee getEmployeeWithSalary() {
int nextNumber = random.nextInt(4);
}
7. Write an application that calls the getEmployeeWithSalary method multiple times
from inside for loop. This method will work like the one in the previous activity:
print the net salary and tax for all employees. If the employee is an instance of
GenericSalesWithCommission also print his commission.
public class UseAbstractClass {
if (f < min)
min = f;
}
System.out.println("The lowest number in the array is " +
min);
}
}
}
3. Create a new CalculatorWithDynamicOperator class with three fields: operand1 and
operator2 as double and operator of type Operator:
public class CalculatorWithDynamicOperator {
students.add(james);
students.add(mary);
students.add(jane);
Iterator studentsIterator = students.iterator();
while (studentsIterator.hasNext()){
Student student = (Student) studentsIterator.next();
String name = student.getName();
System.out.println(name);
}
students.clear();
}
The output is as follows:
Activity 24: Input a String and Output Its Length and as an Array
Solution:
1. Import the java.util.Scanner package:
import java.util.Scanner;
2. Create a public class called NameTell and a main method:
public class NameTell
{
public static void main(String[] args)
{
3. Use the Scanner and nextLine to input a string at the prompt "Enter your name:"
System.out.print("Enter your name:");
Scanner sc = new Scanner(System.in);
String name = sc.nextLine();
4. Count the length of the string and find the first character:
int num = name.length();
char c = name.charAt(0);
5. Print an output:
System.out.println("\n Your name has " + num + " letters including
spaces.");
System.out.println("\n The first letter is: " + c);
}
}
The output is as follows:
if (option.equalsIgnoreCase("Q")) {
break;
}
3. Collect the user input to decide which action to execute. If the action is Q or q,
exit the loop:
System.out.print("Type first operand: ");
double operand1 = scanner.nextDouble();
4. If the action is anything else, find an operator and request two other inputs that
will be the operands covering them to double:
private static void printOptions() {
System.out.println("Q (or q) - To quit");
System.out.println("An operator. If not supported, will use
sum.");
System.out.print("Type your option: ");
}
}
Call the operate method on the Operator found and print the result to the console.
5. Create for loop from 0 to the length of the string passed into the method. Inside
the for loop, get the character at the current index of the string. Name the vari-
able c. Also create a boolean called isDuplicate and initialize it to false. When we
encounter a duplicate, we will change it to true.
for (int i = 0; i < string.length() ; i++){
char c = string.charAt(i);
boolean isDuplicate = false;
6. Create another nested for loop from 0 to the length() of result. Inside the for
loop, also get the character at the current index of result. Name it d. Compare c
and d. If they are equal, then set isDuplicate to true and break. Close the inner
for loop and go inside the first for loop. Check if isDuplicate is false. If it is,
then append c to result. Go outside the first for loop and return the result. That
concludes our algorithm:
for (int j = 0; j < result.length(); j++){
char d = result.charAt(j);
if (c == d){ //duplicate found
isDuplicate = true;
break;
}
}
if (!isDuplicate)
result += ""+c;
}
return result;
}
7. Create a main() method as follows:
public static void main(String[] args){
String a = "aaaaaaa";
String b = "aaabbbbb";
String c = "abcdefgh";
String d = "Ju780iu6G768";
System.out.println(removeDups(a));
System.out.println(removeDups(b));
System.out.println(removeDups(c));
System.out.println(removeDups(d));
}
System.out.println(removeDups(b));
System.out.println(removeDups(c));
System.out.println(removeDups(d));
}
}
The output is as follows:
Activity 27: Read Users from CSV Using Array with Initial Capacity
Solution:
1. Create a class called UseInitialCapacity with a main() method
public class UseInitialCapacity {
public static final void main (String [] args) throws Exception {
}
}
2. Add a constant field that will be the initial capacity of the array. It will also be used
when the array needs to grow:
private static final int INITIAL_CAPACITY = 5;
3. Add a static method that will resize arrays. It receives two parameters: an array of
Users and an int that represents the new size for the array. It should also return
an array of Users. Implement the resize algorithm using System.arraycopy like you
did in the previous exercise. Be mindful that the new size might be smaller than
the current size of the passed in array:
private static User[] resizeArray(User[] users, int newCapacity) {
User[] newUsers = new User[newCapacity];
int lengthToCopy = newCapacity > users.length ? users.length :
newCapacity;
users[users.length - 1] = User.fromValues(row);
} // end of while
return users;
}
5. In the main method, call the load users method and print the total number of users
loaded:
User[] users = loadUsers(args[0]);
System.out.println(users.length);
6. Add imports:
import java.io.BufferedReader;
import java.io.FileReader;
The output is as follows:
27
4. Create another method that reads data from the CSV and load the wages into a
Vector. The method should return the Vector at the end:
private static Vector loadWages(String pathToFile) throws Exception {
Vector result = new Vector();
FileReader fileReader = new FileReader(pathToFile);
BufferedReader bufferedReader = new BufferedReader(fileReader);
try (CSVReader csvReader = new CSVReader(bufferedReader, false)) {
String [] row = null;
while ( (row = csvReader.readRow()) != null) {
if (row.length == 15) { // ignores empty lines
result.add(Integer.parseInt(row[2].trim()));
}
}
}
return result;
}
5. In the main method, call the loadWages method and store the loaded wages in a
Vector. Also store the initial time that the application started:
Vector wages = loadWages(args[0]);
long start = System.currentTimeMillis();
6. Initialize three variables to store the min, max and sum of all wages:
int totalWage = 0;
int maxWage = 0;
int minWage = Integer.MAX_VALUE;
7. In a for-each loop, process all wages, storing the min, max and adding it to the
sum:
for (Object wageAsObject : wages) {
int wage = (int) wageAsObject;
totalWage += wage;
if (wage > maxWage) {
maxWage = wage;
}
if (wage < minWage) {
minWage = wage;
}
}
8. At the end print the number of wages loaded and total time it took to load and
process them. Also print the average, min and max wages:
System.out.printf("Read %d rows in %dms\n", wages.size(), System.
currentTimeMillis() - start);
System.out.printf("Average, Min, Max: %d, %d, %d\n", totalWage / wages.
size(), minWage, maxWage);
9. Add imports:
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Vector;
4. Add imports:
import java.io.IOException;
import java.util.Vector;
The output is as follows:
Bill Gates - [email protected]
Jeff Bezos - [email protected]
Marc Benioff - [email protected]
Bill Gates - [email protected]
Jeff Bezos - [email protected]
Sundar Pichai - [email protected]
Jeff Bezos - [email protected]
Larry Ellison - [email protected]
Marc Benioff - [email protected]
Larry Ellison - [email protected]
Jeff Bezos - [email protected]
Bill Gates - [email protected]
Sundar Pichai - [email protected]
Jeff Bezos - [email protected]
Sundar Pichai - [email protected]
Marc Benioff - [email protected]
Larry Ellison - [email protected]
Marc Benioff - [email protected]
Jeff Bezos - [email protected]
Marc Benioff - [email protected]
Bill Gates - [email protected]
Sundar Pichai - [email protected]
Larry Ellison - [email protected]
Bill Gates - [email protected]
Larry Ellison - [email protected]
Jeff Bezos - [email protected]
Sundar Pichai - [email protected]
6. In the main method, call your loadWages method passing the first argument from
the command line as the file to load the data from:
Hashtable<String,Vector<Integer>> wagesByEducation = loadWages(args[0]);
7. Iterate on the Hashtable entries using a for-each loop and for each entry, get the
Vector of the corresponding wages and initialize min, max and sum variables for it:
for (Entry<String, Vector<Integer>> entry : wagesByEducation.entrySet()) {
Vector<Integer> wages = entry.getValue();
int totalWage = 0;
int maxWage = 0;
int minWage = Integer.MAX_VALUE;
}
8. After initializing the variables, iterate over all wages and store the min, max and
sum values:
for (Integer wage : wages) {
totalWage += wage;
if (wage > maxWage) {
maxWage = wage;
}
if (wage < minWage) {
minWage = wage;
}
}
9. Then, print the information found for the specified entry, which represents an
education level:
System.out.printf("%d records found for education %s\n", wages.size(),
entry.getKey());
System.out.printf("\tAverage, Min, Max: %d, %d, %d\n", totalWage / wages.
size(), minWage, maxWage);
10. Add imports:
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Hashtable;
import java.util.Map.Entry;
import java.util.Vector;
5. After loading the users, transfer the users into a Vector of Users to be able to
preserve order since Hashtable doesn't do that:
Vector<User> users = new Vector<>(uniqueUsers.values());
6. Ask the user to pick what field he wants to sort the users by and collect the input
from standard input:
Scanner reader = new Scanner(System.in);
System.out.print("What field you want to sort by: ");
String input = reader.nextLine();
7. Use the input in a switch statement to pick what comparator to use. If the input is
not valid, print a friendly message and exit:
Comparator<User> comparator;
switch(input) {
case "id":
comparator = newByIdComparator();
break;
case "name":
comparator = new ByNameComparator();
break;
case "email":
comparator = new ByEmailComparator();
break;
default:
System.out.printf("Sorry, invalid option: %s\n", input);
return;
}
8. Tell the user what field you're going to sort by and sort the Vector of users:
System.out.printf("Sorting by %s\n", input);
Collections.sort(users, comparator);
9. Print the users using a for-each loop:
for (User user : users) {
System.out.printf("%d - %s, %s\n", user.id, user.name, user.email);
}
Node getNext() {
return next;
}
void setNext(Node node) {
next = node;
}
Object getData() {
return data;
}
}
3. Implement a toString() method to represent this object. Starting from the
head Node, iterate all the nodes until the last node is found. On each iteration,
construct a string representation of the object stored in each node:
public String toString() {
String delim = ",";
StringBuffer stringBuf = new StringBuffer();
if (head == null)
return "LINKED LIST is empty";
Node currentNode = head;
while (currentNode != null) {
stringBuf.append(currentNode.getData());
currentNode = currentNode.getNext();
if (currentNode != null)
stringBuf.append(delim);
}
return stringBuf.toString();
}
4. Implement the add(Object item) method so that any item/object can be added
into this List. Construct a new Node object by passing the newItem = new
Node(item) Item. Starting at the head node, crawl to the end of the list. In the last
node, set the next node as our newly created node (newItem). Increment the index:
// appends the specified element to the end of this list.
public void add(Object element) {
// create a new node
Node newNode = new Node(element);
//if head node is empty, create a new node and assign it to Head
//increment index and return
if (head == null) {
head = newNode;
return;
}
Node currentNode = head;
// starting at the head node
// move to last node
while (currentNode.getNext() != null) {
currentNode = currentNode.getNext();
}
// set the new node as next node of current
currentNode.setNext(newNode);
}
5. Implement get(Integer index) method to retrieve the item from the list based on
the index. Index must not be less than 0. Write a logic to crawl to the specified
index, identify the node, and return the value from the node.
public Object get(int index) {
// Implement the logic returns the element
// at the specified position in this list.
if (head == null || index < 0)
return null;
if (index == 0){
return head.getData();
}
Node currentNode = head.getNext();
for (int pos = 0; pos < index; pos++) {
currentNode = currentNode.getNext();
if (currentNode == null)
return null;
}
return currentNode.getData();
}
6. Implement the remove(Integer index) method to remove the item from the list
based on the index. Write logic to crawl to the node before the specified index
and identify the node. In this node, set the next as getNext(). Return true if the
element was found and deleted. If the element was not found, return false:
public boolean remove(int index) {
if (index < 0)
return false;
if (index == 0)
{
head = null;
return true;
3. In the main method, construct a BST, add values to it, and then print the highest
and lowest values by calling getLow() and getHigh():
/**
* Main program to demonstrate the BST functionality.
* - Adding nodes
* - finding High and low
* - Traversing left and right
* @param args
*/
public static void main(String args[]) {
BinarySearchTree bst = new BinarySearchTree();
// adding nodes into the BST
bst.add(32);
bst.add(50);
bst.add(93);
bst.add(3);
bst.add(40);
bst.add(17);
bst.add(30);
bst.add(38);
bst.add(25);
bst.add(78);
bst.add(10);
//printing lowest and highest value in BST
System.out.println("Lowest value in BST :" + bst.getLow());
System.out.println("Highest value in BST :" + bst.getHigh());
}
The output is as follows:
Lowest value in BST :3
Highest value in BST :93
System.out.println(DeptEnum.BE == DeptEnum.valueOf("BE"));
}
}
4. Output:
BACHELOR OF ENGINEERING : 1
BACHELOR OF ENGINEERING : 1
BACHELOR OF COMMERCE : 2
BACHELOR OF SCIENCE : 3
BACHELOR OF ARCHITECTURE : 4
BACHELOR : 0
True
4. Declare a public method getAccronym() that returns the variable accronym and a
public method getDeptNo() that returns the variable deptNo.
public String getAccronym() {
return accronym;
}
public int getDeptNo() {
return deptNo;
}
5. Implement reverse look up that takes in the course name, and searches the corre-
sponding acronym in the App enum.
//reverse lookup
public static App get(String accr) {
for (App e : App.values()) {
if (e.getAccronym().equals(accr))
return e;
}
return App.DEFAULT;
}
6. Implement the main method, and run the program.
public static void main(String[] args) {
while (true) {
System.out.print("Enter name of visitor: ");
String name = input.nextLine().trim();
if (name.length() == 0) {
break;
}
6. The try block, read the age of the visitors, throws TooYoungException if the age is
below 15, prints the name of the visitor riding the Roller Coaster:
try {
System.out.printf("Enter %s's age: ", name);
int age = input.nextInt();
input.nextLine();
if (age < 15) {
throw new TooYoungException(age, name);
}
while (true) {
System.out.print("Enter name of visitor: ");
String name = input.nextLine().trim();
if (name.length() == 0) {
break;
}
7. The try block, read the age of the visitors, throws TooYoungException if the age is
below 15, TooShortException if the height is below 130, and prints the name of the
visitor riding the Roller Coaster:
try {
System.out.printf("Enter %s's age: ", name);
int age = input.nextInt();
input.nextLine();
if (age < 15) {
throw new TooYoungException(age, name);
}
System.out.printf("Enter %s's height: ", name);
int height = input.nextInt();
input.nextLine();
if (height < 130) {
throw new TooShortException(height, name);
}
while (true) {
System.out.print("Enter name of visitor: ");
String name = input.nextLine().trim();
if (name.length() == 0) {
break;
}
7. The try block, read the age of the visitors, throws TooYoungException if the age is
below 15, TooShortException if the height is below 130, and prints the name of the
visitor riding the Roller Coaster:
try {
System.out.printf("Enter %s's age: ", name);
int age = input.nextInt();
input.nextLine();
if (age < 15) {
throw new TooYoungException(age, name);
}
System.out.printf("Enter %s's height: ", name);
int height = input.nextInt();
input.nextLine();
if (height < 130) {
throw new TooShortException(height, name);
}
About
All major keywords used in this book are captured alphabetically in this section. Each one is
accompanied by the page number of where they appear.
W
wrapper: 100, 112, 140
wrapping: 167, 248, 282