4. Syntax & Semantics
Since Java is based largely upon the C/C++ class of languages, those familiar with these languages will find this chapter mostly a review. The designers of Java had a simple objective: They wanted a language that was close to C/C++, one that carried that language's strengths into the realm of Internet and Intranet programming. But to use it as intended, Java had to resolve weaknesses of C/C++ in the areas of security, portability and maintenance. They also added multiple threading and exception handling to make Internet programming easier. Thus, the differences between C/C++ and Java generally fall into these categories.
Most of the information in this chapter was culled from the Java Language Specification for release 1.0. Java is still changing, so you can expect changes in future versions. The language design still incorporates some unused keywords, for example, and Sun has already mentioned some possible changes and additions to the language. The Online Companion for this book will be updated as these changes occur.
Although this chapter is meant to serve as a reference that you can turn to again and again during programming, it does provide important basic information about Java. We recommend that you at least scan this chapter before proceeding to the rest of the book. Be sure to read the sections on Arrays and Exceptions carefully. Java handles Arrays and Exceptions in ways you might not expect based on your knowledge of other languages. The concept of Exceptions, in particular, is a key concept that is used throughout the book.
In this chapter, we discuss Java syntax for the following elements, in this order:
Identifier is the technical term for what we use to name elements in our programs. Anything we create in our Java programs-be it a variable, method, or class-is named using an identifier. Identifiers are composed of a sequence of Unicode characters. "Unicode? Never heard of it," you might say. The truth of the matter is that after this chapter, you may never hear of it again.
Java was designed to be portable, not only across computer platforms, but also across national barriers. Multilanguage support has become a hot topic as companies race to access global markets. Currently most computer programs are written in English. A programmer in a non-English-speaking country is forced to program in a foreign language. Computers are already foreign to many people; adding another level of complexity can make learning to program even harder. This is where Unicode comes in.
Unicode has been developed by the Unicode Consortium and was first publicly released in 1990. It is designed to encode most modern and historic languages. Each character is stored in 16 bits. Most users are probably familiar with the ASCII character set, which uses 7 bits to define each character. Unicode uses the extra bits to add many language-specific characters and to provide diacritics. Diacritics are symbols used to show how a word is accented. Many non-English languages use diacritics. Java programs are written in Unicode, and all strings and characters are stored as 16-bit Unicode characters.
Will you have to learn a new character set? The answer is no. Rest assured that Unicode will not greatly affect your programming. If you program in English, you can continue programming as you already do. The Java compiler will convert any ASCII file into an equivalent Unicode file. Furthermore, your source files won't be affected by the change to Unicode.
Sun's decision to use Unicode has no significant effect on the average programmer. Certain items may take up more memory-specifically, all strings will be twice as large. This might seem bad, but remember that Java is not designed to be superefficient in speed or memory. It's designed to make Internet programming easier and to make programs portable. These advantages make the extra memory requirements somewhat inconsequential. Be aware that characters take up more space, but don't stress over the fact. When you move on to internationalizing your product, you'll stop cursing and soon begin to sing.
If you would like more information on Unicode, point your Web
browser to http://unicode.org. Here you will find information about
the Unicode standard, how to get hard copies of the standard, and
membership information for the organization.
Java supports C-style comments and adds a new style for automatically creating online documentation. It doesn't matter which commenting style you use; they will all have the same effect. Any information specified by commenting characters will not be processed by the compiler.
The common C-style comments are as follows:
Java has added a third commenting style, which is used in automatic program documentation. These comments are processed by javadoc, a program supplied with the Java language that creates Web pages describing your code. The comments supplied in this new format are included in the Web pages. Comments used in automatic documentation use the following format:
/** text */
This documents a coming variable or method.
Every language designer is faced with decisions about comment nesting and other compiler issues. It seems that each language handles comments in its own way and Java is no exception. The rules dealing with comments are as follows:
One example will serve to explain what all these rules mean. The following will be treated as one legal comment:
/* Normal Comment with // /* /** characters inside. It ends here */
Every language has a group of words that the compiler reserves for its own use. These keywords cannot be used as identifiers in your programs. Table 4-1 lists the keywords reserved for the Java compiler. Those items marked with an asterisk are reserved for future use. Some, such as const and goto, are reserved for better error messages, and others indicate concepts that Sun may include in later versions of the language.
Java Keywords | ||||
---|---|---|---|---|
abstract | do | implements | package | throw |
boolean | double | import | private | throws |
break | else | *inner | protected | transient |
byte | extends | instanceof | public | try |
case | final | int | *rest | *var |
*cast | finally | interface | return | void |
catch | float | long | short | volatile |
char | for | native | static | while |
class | *future | new | super | ** strictfp |
*const | *generic | null*** | switch | |
continue | *goto | operator | synchronized | |
default | if | *outer | this | |
A variable can be one of four kinds of data types: classes, interfaces, arrays, and primitive types. At compile time, the compiler classifies each variable as either a reference or a primitive value. A reference is a pointer to some object. Classes, interfaces, and arrays are stored as references. Primitive types are data types that include integers, floating point numbers, characters, and booleans. Primitive data types are stored directly; their size is easily determined and never changes.
For information on primitive types, see the next section, "Primitive
Data Types." For information about the reference types, see the
sections, "Classes & Interfaces" and "Arrays".
Primitive types are the heart of any programming language. They are the types the compiler specifically knows about; every element of a user-defined type can be decomposed to a primitive type. They are building blocks that will be required in all but the simplest program. Let's explore each primitive type and see what the Java language is made of.
The Java designers have already defined the sizes for each data type.
There are no machine-dependent sizes for a type as in C and C++,
so there is no need for a sizeof function. Finally, a glimmer of hope
for true portability!
An integer is a whole number with no fractional value. It can be modified only in whole increments. Most computer operations are done on integer values. The integer type is further identified by its size. A Java integer can range from 8 bits to 64 bits. The type of integer you use determines its minimum and maximum values. Java doesn't support unsigned types, so you may need to use a type you don't ordinarily use. Table 4-2 shows characteristics of each integer type.
Name | Size (bits) | Minimum | Maximum | Example |
---|---|---|---|---|
byte | 8 | -128 | 127 | 16,-16 |
short | 16 | -32768 | 32767 | 99, -99 |
int | 32 | -2147483648 | 2147483648 | 99999, -99999 |
long | 64 | -922372036854775808 | 922372036854775807 | 100000000000 |
Integer literals can be represented in a program in one of three bases: You can enter them in base 10 decimal, base 16 hexadecimal, or base 8 octal. All numbers are assumed to be in decimal unless otherwise specified. All literals are of the type int, unless you cast them to other values or end them in the letter l , meaning "long."
A hexadecimal digit can range from 0 to 15. A digit in a decimal system ranges from 0 to 9. To represent the numbers from 10 to 15 in one digit, a letter is used. The letters a through f represent 10 through 15 respectively. Hexadecimal numbers are used frequently to specify large numbers or to represent binary coded values. Since each digit can represent 16 possible values, a large number can be written in a more compact form.
Take, for example the number 32767, represented in the decimal system. This is the largest value possible for a short. It would be represented in hexadecimal by Ox7FFF. Any time you wish to use a hexadecimal number, you must preface it with Ox. The case of the letters is not significant.
An octal digit ranges from 0 to 7. A number in the octal system is specified by a leading zero followed by zero or more octal digits. For instance, 32767 in decimal would be represented as 077777 in octal. The leading zero tells the compiler to expect an octal number.
Integers exhibit a property called wrapping. If you try to increment or decrement an integer so that it overflows or underflows, the number will wrap. Consider a byte with a value of 127. If you increment this byte by 1, its value will be -128. That's right-the number went from the largest positive to the smallest negative number. The reverse would happen if you subtracted 1 from -128. Know what range of numbers you want to support, and choose an integer of the appropriate size.
Java implements single and double precision floating point numbers as specified in IEEE Standard for Binary Floating-Point Arithmetic. Two forms are supported: a float and a double. A float is a 32-bit single precision floating point number. A double is stored in 64 bits and is a double precision number.
In addition to storing a value, a floating point can have a few specially defined states: negative infinity, negative zero, positive zero, positive infinity and not-a-number (NaN). Since these values are defined by the Java language, you can check for them in your code. Generally, they will only occur in special situations-for example, when zero is divided by zero. The result is specified as NaN and can be explicitly checked for. Error checking is much easier when the language supports these states.
All floating point literals are assumed to be of type double unless otherwise specified. To make a number a 32-bit float, follow it with the letter f. This will tell the compiler that you want this literal stored as a float. Since Java enforces type checking, you will have to do this to initialize a floating point variable.
The following code fragment will generate a compiler error due to a type mismatch:
float num = 1.0;
Since all floating point literals are assumed to be doubles, you need to specify that this is a float literal by placing an f after the value. The corrected code from above is:
float num = 1.0f;
One cautionary note about floating points: using them in control
statements can be confusing. Two floating point numbers can be
equal for many decimal places, but if they are not exactly equal,
they will cause bugs by not evaluating as equal. Comparing floats
in an if statement or using a floating pointer variable as a loop
counter is a sure way to decrease speed and create subtle bugs.
Characters are implemented using the Unicode standard (see "Identifiers & Unicode" earlier in this chapter), meaning that each character is stored in 16 bits. Unicode allows you to store many different nonprinting or foreign characters. To specify a character literal, you can use either a normal character or a Unicode escape sequence. In either method, you'll need to enclose the value in a pair of single quotes.
Unicode escape sequences can be specified using two methods. The first format will be familiar to C/C++ programmers. You can specify commonly used escape sequences by a backslash (\) followed by a letter from Table 4-3.
Escape | Function | Unicode |
---|---|---|
\b | Backspace | \u0008 |
\t | Horizontal Tab | \u0009 |
\n | Linefeed | \u000a |
\f | From Feed | \u000c |
\r | Carriage Return | \u000d |
\" | Double Quote | \u0022 |
\' | Single Quote | \u0027 |
\\ | Backslash | \u005c |
You can also specify escape sequences typing \u followed by a four-digit hexadecimal number (the specific Unicode number assigned for the character you want). The number can range from \u0000 to \u00ff. A few examples of character literals are:
Booleans are variables that have two states: true or false. The only way to assign a value to a boolean variable is with the literals, true and false. Unlike with C, you cannot assign integers to a boolean. To simulate C's automatic type conversion, you can compare the integer to 0. The C language states that 0 is false and all other values are true. In converting the integer i to a boolean, you might use the following Java code:
int i; boolean b; b = (i != 0);
Here, we are using the not-equals operator to see if i is equal to zero. The parentheses are required for the expression to evaluate in the proper order.
Booleans are an important concept in Java. There are many
language constructs, such as loops and if statements, that accept only
boolean expressions. They are easy to understand, but if you are
used to C-style expressions, booleans may take some getting used to.
Primitive Data Type Conversion
Converting between two primitive data types is a common practice. The trick to converting one variable to another is making sure you understand what is going on. If you're not careful, you might lose information or get a result you were not expecting.
Java enforces strict type checking. The compiler will not automatically convert from one type to another. You need to explicitly convert to another type by using a mechanism called a type cast, which will tell the compiler to convert from one type to another.
In Java, type casts are done the same way they are done in C/C++. You specify a type name by enclosing it in parentheses. If the type conversion is supported, the result of the computation or assignment will be the typed value. Suppose you have two variables, shortVar and intVar (shortVar is of type short; intVar is of type int). There are two possible type conversions between these types:
short shortVar=0; int intVar=0; intVar = shortUar; shortVar = intVar; // Incompatible type for equals
When this code is compiled, assigning shortVar to intVar results in an incompatible type error. You are trying to assign a larger variable (intVar, of type int) to a smaller variable (shortVar of type short). This type of conversion is called a narrowing conversion, in which you try to convert to a type that contains fewer bits than the original. When performing a narrowing conversion, you may lose magnitude or precision information. Java makes you explicitly state that you understand this may happen: you must type cast all narrowing conversions. This is another case in which the Java language tries to force good programming conventions. Any time you lose bits, for instance, by converting from a long (64 bits) to an int (32 bits), you must make an intelligent decision about handling the lost magnitude information. If you are sure that the long won't be too large, use a cast; otherwise, you'll need to write some code to inform the user of the lost magnitude information.
The code we presented above can be made to work by adding a type cast. The correct code would be:
short shortVar=0; int intVar=0; intVar = shortVar; shortVar = (short) intVar;
The compiler will now allow you to make the assignment. The integer, intVar will be converted to a short by dropping the high-order bits while preserving the sign of the number. In this case, we are going from a 32-bit integer to a 16-bit short. The upper 16 bits will be lost.
Table 4-4 shows the possible primitive type conversions in Java. An entry of C means that you must use an explicit cast, or you will get a compiler error. An L means that you may lose magnitude or precision in the conversion. An X means this type of conversion is not allowed in Java.
Original Type | Destination Type | |||||||
---|---|---|---|---|---|---|---|---|
byte | short | int | long | float | double | char | boolean | |
byte | C | X | ||||||
short | C, L | C | X | |||||
int | C, L | C, L | C, L | X | ||||
long | C, L | C, L | C, L | C, L | C | C, L | X | |
float | C, L | C, L | C, L | C, L | C, L | X | ||
double | C, L | C, L | C, L | C, L | C, L | C, L | X | |
char | C, L | C | C | C | C | C | C | X |
boolean | X | X | X | X | X | X | X | |
When converting from a floating point number to any integer type, you will lose all fractional information. Java truncates the value by rounding toward zero. It will then convert the resulting integer by dropping or adding bits as needed.
A conversion from a double to a float will round according to IEEE 754 round-to-nearest mode. Values that have too large a magnitude will result in either positive or negative infinity NaN (Not a Number) will always convert to NaN.
No type may be converted to or from a boolean variable. If you want to convert from an integer to a boolean or a boolean to a string, you must manually convert these values. You might use the following code:
boolean bool; int i=15; String st=null; if (i == 0) bool = false; else bool = true; if (bool) st = "true"; else st = "false";
For information about converting reference data types (classes,
interfaces, and arrays), see "Objects" later in this chapter.
A variable in Java can be a primitive type, an object, or an interface. Variables may be created in any position that would be appropriate for a statement. Any variable declaration can be followed by an initialization statement, which sets an initial value for the variable.
As we've seen in earlier examples, defining a variable is an easy task. Let's define and initialize some Java variables:
int i=42; String st="Hello World"; float pi=3.14f; boolean cont;
Notice that we said a variable can be created in any position that is
appropriate for a statement. It isn't necessary to put your variable
declarations first-you can put them where you use the variable.
Every variable declaration has a lifetime, or scope, that is based on where the variable is declared. When you place a block of code within curly braces {}, you have defined a new scoping level. The scoping level determines when a variable is deallocated and where it is accessible. A variable is only accessible if it was declared in the current scope block or declared by one of its parents.
When you leave a scoping level, any variables declared in that block become inaccessible. They may or may not become deallocated, since the rules for deallocation are a little more complex than those for accessibility. When a variable is declared, it has space allocated for it. When the current scope block is ended, the variable is available for deallocation. It's up to the garbage collector to decide when a variable is actually deallocated. This will happen only when there are no more references to the variable. So, for primitive data types, deallocation will occur when the scope block ends. For reference variables, it might be sometime later.
Scoping rules are easier to understand if you think of them as branches on a tree-each new block creates a new branch in the tree. The more levels of blocks there are, the taller the tree will be. Let's take a piece of code and draw its scoping tree:
class foo { int cnt; public void test1() { int num; } public void test2() { for(int cnt=0; cnt < 5; cnt++) { System.out.println(cnt); } } }
Figure 4-1 shows the scoping tree for the preceding code.
Figure 4-1: Scoping tree.
Notice that each new block creates a new scoping entry. We start at the package level, where all the classes and interfaces are declared. At the next level, we have the class variables and the method names for the class. Each class method starts its own scoping level, and its local variables are defined there. Notice, in particular the case of the method test2: It has no local variables declared in the first scope, but the for loop creates a new scope and defines a variable called cnt. This variable shadows the previous declaration of cnt on the class level.
For non-C programmers, the for loop may be new. We will explain for loops and many other loop constructions in the "Control Flow" section later in this chapter.
Having multiple levels of scoping creates the ability to shadow, or hide, other variables. Let's say you created a class variable cnt. Then, in a method for the class, you also created a variable cnt. The new variable would shadow or hide, the existence of the class variable cnt. Any references in the method to cnt would be to the newly created local variable. To access the class variable cnt, you would use a predefined class variable called this. The this variable is just a reference to the class. Any time you need to access a shadowed variable, use the this variable to point to the shadowed class variable. In our example, we could access the class variable cnt in the following way:
this.cnt = 4;
As a general rule, it's best to avoid shadowing a variable. If you
must shadow it, you will have to live without the class declaration
or use the this variable to access it.
A variable name must start with a letter. It can be of unlimited length and consist of letters, digits, and any punctuation character except the period. A variable cannot duplicate any other identifiers on the same level of scoping. This means that you can't have a variable with the same name as:
If you're using the Unicode character set to provide foreign language support, then you need to be aware of how Java handles character comparisons. An identifier will be equal to another if it has the exact same sequence of Unicode characters. This means that a Latin capital A (\u0041) is distinct from a Greek capital A (\u0391) and all other representations of the letter A. It follows, then, that the Java language is case-sensitive, a capital A has a different value then a lowercase a .
Java supports a wide range of operators. An operator is a symbol used to perform a particular function on one or more variables. Some common operators are plus (+), minus (-), and equals (=). Operators are classified by the number of arguments they accept. Some operators, such as minus, have different meanings based on whether they have one or two operands.
. | [] | () | ||
++ | -- | ! | ~ | instanceof |
* | / | % | ||
+ | - | |||
<< | >> | >>> | ||
< | > | <= | >= | |
== | != | |||
& | ||||
^ | ||||
| | ||||
&& | ||||
|| | ||||
?: | ||||
= | op= | |||
, | ||||
The operator op= is shorthand for the a class of equality operators.
An example is +=.
An expression is a combination of operators and operands. The evaluation of an expression is governed by a set of precedence rules. To say that one operator has precedence over another means that it will be evaluated first. A common example from grade school mathematics is the difference between addition and multiplication. We can scan the expression x = 2 + 4 * 3 and know that x equals 14, not 18. The multiplication operator is evaluated before the plus operator.
For complicated expressions, you are better off just using parentheses to denote the order of operations. Not only will you be sure it's correct, but other programmers will appreciate your clarity. Table 4-5 is provided to answer questions of precedence. Items that appear on the same line are of equal precedence and will be evaluated from left to right.
Operators that act on numbers fall into two categories: unary operators, which act on one variable, and binary operators, which act on two. The binary operators can be further divided into those that compute a numerical result and those that are used to compare values.
The result of an operator will always be of the same type as the largest operand. Let's say that we are adding two numbers-one a short, the other a long. The result of the addition would be a long. Table 4-6 shows how this works. Notice that the smallest result returned for integers is an int. Any number added to a floating point number will be a float or double. Now that we know what the resultant type will be, let's examine the unary operators.
Type 1 | Type 2 | Resultant |
---|---|---|
byte | byte | int |
byte | short | int |
byte | int | int |
byte | long | long |
short | short | int |
short | int | int |
short | long | long |
int | int | int |
int | long | long |
int | float | float |
int | double | double |
float | float | float |
float | double | double |
A unary operator takes one operand as its parameter. The operation is performed, and the result is placed in the operand. The resulting type will always be the same as the original type, and there is no chance of a loss of precision or magnitude. Table 4-7 lists the unary operators in Java.
Operator | Description |
---|---|
- | Unary Minus |
+ | Unary Plus |
~ | Bitwise Complement |
++ | Increment |
-- | Decrement |
Some possible unary expressions are:
Unary Minus & Plus
The unary minus operator (-) is used to change the sign of a number. A negative number becomes positive, and a positive number becomes negative. The unary plus operator actually performs no work. It is there for completeness.
Integer negation can be thought of as subtraction from zero. This holds true for all but the largest negative numbers; unary minus has no effect on these numbers. The reason for this is that the negative numbers have one more value than the positive numbers. To illustrate this, let's look at a piece of code. It will output a possibly unexpected result of -128:
byte i=-128; System.out.println(-i);
When dealing with floating point numbers, there are a few extra considerations. A number with the value of NaN will still be NaN afterward. Positive and negative zero will do as expected and change sign. This also holds true with positive and negative infinity.
Bitwise Complement
The bitwise complement operator only works on integer type variables. It looks at an integer on the bit level, where it changes 0s to 1s and 1s to 0s. If you were performing this operation on a variable named x, then ~x would be equivalent to (-x) - 1. Those not familiar with bit-level manipulations may have never seen this operator. It is more commonly used when looking at a variable as a collection of separate bits than as a complete number. This operator will flip each bit. If you have an integer that represents the value 0, then its complement would be 65535.
Increment & Decrement
The increment and decrement operators are shorthand for adding and subtracting 1. They can be placed either before or after a variable. The placement determines whether it is a prefix or a postfix operator. A prefix operator will return the value of its operand after evaluating the expression. A postfix operator returns the operand and then performs the evaluation. Let's look at a piece of code to see this concept in action:
int i=0; int j=0; System.out.println(++i); System.out.println(j++);
The output of this program will be 1 and then 0. In the first print statement, we've used a prefix operator. The variable i will be incremented, and then it will be printed. In the second case, the variable j will be printed and then incremented. Note that in both cases, i and j will be equal to 1 at the end.
The binary operators take two operands, perform an operation, and return a result. The result is the same type as the largest operand. Adding a byte and an int will result in an int. The operation will not affect the variables involved. We can divide the binary operators into those that compute some numerical value and those that compare two numbers. Those that compute numerical results are listed in Table 4-8.
Operator | Description |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulus |
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
<< | Signed Left Shift |
>> | Signed Right Shift |
>>> | Unsigned Right Shift |
op= | Combination assignment and operation |
Additive Operators
The additive operators are + and -. If either operand is a floating point number they will both be treated as floating points. Any integer types besides a long will be converted to an int. This means that the additive operators will never return a byte or short value. To assign the result to a byte or short, you'll have to use an explicit cast. Java follows IEEE rules for floating point addition and subtraction. This does what you expect, but you might need to check the Java Language Specification for information on how Java handles special cases, such as adding two infinite numbers.
Multiplicative Operators
The multiplicative operators are *, /, and %. They convert operands in a manner similar to that of the additive operators. Operations on integers are associative, except for operations on floating point numbers. For example, imagine we have two floating point numbers. One is positive 1; the other is the largest representable positive number:
float one = 1f; float max = 2^24e104;
Given this setup, the following expression would not be true:
one + max - one == max - one + one
In the first case, we are adding 1 to the largest representable float. This will cause an overflow, and the result of that sub-expression will be positive infinity. Subtracting 1 from infinity still produces infinity, so the result of the left-hand expression is positive infinity. The right-hand expression subtracts 1 from the max, then adds 1, yielding a value of max.
The remainder (%), or modulus, operator is defined such that (a/b) * b + (a%b) = a. This operator does what you expect-it gives you a positive result when the dividend is positive and a negative result when a negative operand is used. The result is always less than the magnitude of the divisor.
An example of the modulus operator and its behavior is:
int i=10; int j=4; int k=-4; System.out.println(i % j); // = 2 System.out.println(i % k); // = -2
The behavior of the Java modulus operator is different than specified
by IEEE 754. If you want to use that definition, you can access it
from the math library as Math.IEEEremainder.
Bitwise Operators
The bitwise operators are used to perform operations concerned with how a number is represented in bits. They only make sense when dealing with integer numbers. Floating point numbers are not simple encodings of bits; therefore the bitwise operations would not make sense on floating points. It is common practice to use the bitwise operators and the and (&) , or ( I ), and xor (^) operators to check individual bits of an integer.
Let's assume we are using each bit of a byte to store a flag. We can store eight flags-one for each bit. In the following code, we'll show you some common bit-manipulation operations:
byte flags=0xff; // Initialize to 11111111 byte mask=0xfe; // Set mask to 11111110 flags = flags & mask; // Set position 8 flags = flags | mask; // Clear position 8 flags = flags ^ mask; // = 00000001
Shift Operators
Java supports three shift operators-left shift (<<), right shift (>>), and the unsigned right shift (>>>). The form for all shift operations is a shift expression, the shift operator and the distance to shift. The shift expression must be an integer type. If you want to perform a shift operation on a floating point number, you'll have to perform an explicit conversion.
The signed shift operators will preserve the sign on the operand. The unsigned right-shift operator will use zero-extension. ( Zero-extension means that it will replace each shifted position with a zero, ignoring the sign bit.) Shifting by a value of zero is allowed, but it serves no purpose.
Using the shift operators, we can perform an easy integer division by 2. A right shift will cause a loss of 1 bit. Right-shifting by 1 is equivalent to dividing the number by 2. For odd numbers, this operation will round down:
int i = 129; // This is 10000001 in binary i = i >> 1; // Now this is 1000000 or 64
Combination Assignment
Operators Some operators can be combined into a combination shorthand that does the operation and then assigns the result to the operand. The operators that can be combined in this manner are those that perform a numerical computation-that is, all the Java operators except the relational operators (see "Relational Operators" below). You need to be aware of how operands are loaded when using combination operators. If you change the resultant variable in the combination expression, it will not affect the result. This concept is best shown with a code example:
int i = 0; i += ++i;
You might think the code would yield a value of 2. The outcome may surprise you. First, i is loaded into a register. The expression ++i is calculated, assigned back to i , and used as the second operand to the plus operator. When the two operands are added together we get 0, the original value of i , and 1. This results in a value of 1.
Relational Operators
In addition to numerical computing operators, there is a group of operators that compare two values. They are relational operators-they take two parameters and return a boolean result stating their relationships. The relational operators in Java are described in Table 4-9.
Operator | Description |
---|---|
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
== | Equal to |
!= | Not equal to |
There is a common pitfall when using the relational operators.
Invariably, programmers use a single equals sign instead of a
double equals sign in a statement. A single equals sign is used to
assign one value to another. A double equals sign is used to
compare two numbers. Unlike with C/C++, Java will catch this error,
but it is much easier not to make the mistake in the first place.
Boolean Operators
Boolean operations are very similar to comparable operations on numbers. All of the boolean operators will return booleans as their result. For a list of the boolean operators in Java, see Table 4-10.
Operator | Description |
---|---|
! | Negation |
& | Logical AND |
| | Logical OR |
^ | Logical XOR |
&& | Conditional AND |
|| | Conditional OR |
== | Equal to |
!= | Not equal to |
op= | Combination assignment and operator |
?: | Conditional operator |
The conditional operator is the only ternary operator in the Java language. The operator's format is a?b:c. Expression a is evaluated for a boolean result. If it is true, then b is returned; if not, c is returned. Basically, it is an if statement. There are two ways to write a piece of code:
int i; boolean cont=false; // Conventional if statement if (cont) i=5; else i=6; // Using the short hand i = (cont?5:6);
The above code is setting the value of i to either 5 or 6 based on some boolean variable cont. When cont is true, i is set to 5; when it is false, i is set to 6. The conditional operator gives us a shorthand way to achieve this.
Character Operations
There are no operators that return a character result. Most of the operators we've discussed return an integer type. If you wish to perform an operation on a character you will have to cast the result back to a character. When a character is an operand to an operator, it will be converted to an int. This is a simple conversion that will not lose any information. Suppose we wanted to convert a character from uppercase to lowercase. The integer value of the character A is 98 in both ASCII and Unicode. A lowercase a has a value of 65. If we take an uppercase letter and subtract the difference between 98 and 65, we will get a lowercase letter. Let's use this principle and a bit of Java code to illustrate character operations:
char c='B'; c = (char) (c - ('A' - 'a'));
Objects in Java support the following operators: =, ==, !=, and instanceof. Generally, operations such as adding two objects together make no sense. The only time this type of operation is allowed is in the special case of string addition.
The assignment operator (=) is used to assign an object pointer to a reference variable. This does not create a copy of the object. Instead, after using the equals operator, the reference variable will point to the operand. When all references to an object go out of scope, the object will become available for garbage collection. Assuming that we have a class called foo, the following code shows the use of the equals operator:
foo test = new foo(); foo test2 = null; test2 = test;
In the above code, we have shown the valid uses of the equals operator when dealing with objects. The first line illustrates its use when creating a new instance of an object. The new command returns a reference to a newly created object. In the second line, we assign test2 to null. Test2 now references no object, and any attempt to use test2 will create a NullPointerException. The last line assigns test2 to test. Now both reference variables are pointing to the same object.
Objects support two comparison operators: the equals operator (==) and the not equals operator (!=). These operators test to see whether the objects in question reside in the same place in memory. They don't test each individual component of an object for equality. Two objects with the exact same contents that are different instances will not be equal to each other. For example, we have two instances of some class foo that are defined as follows:
foo test = new foo(); foo test2 = new foo(); foo test3 = test;
Using these definitions, we can create a table of equality relationships between each reference variable. Table 4-11 will show you which operator, equals or not equals, you would use to get a result of true.
test | test2 | test3 | |
---|---|---|---|
test | == | != | == |
test2 | != | == | != |
test3 | == | != | == |
The instanceof operator is used to determine the runtime type of an object. Its use is necessary because you can't determine a reference variable's type at compile time. For example, you might have a situation in which you have a class called shape. A subclass of shape is polygonShape. If you have a variable that holds a shape, how can you tell if it's a polygon? Let's look at a code fragment that would solve the problem:
shape shapeHolder; if (shapeHolder instanceof polygonShape) { polygonShape polygon = (polygonShape) shapeHolder; // Do something with the polygon }
In this example, we have some generic shape. If the shape is of a specific type, polygonShape, then we want to perform some operations on it. In order to access the member functions specific to polygonShape; we need to have a reference variable of the correct type.
The type of situation we've just described happens frequently when you are dealing with a data structure that holds objects that are subclasses of a common parent. Suppose we have an object-based paint program, and that we store all of the shapes the user has drawn in a data structure. To print this data, we will need a loop that will traverse our data structure and print each shape. If a particular shape needs special instruction in order to print, we will need to use the instanceof operator.
As we explain in the "Strings" section of this chapter the string class in Java is a hybrid of a primitive type and an object. It looks like an object to the user, but special cases have been included in the compiler for strings, creating a somewhat confusing dichotomy. There are a few pitfalls associated with operations with strings.
We said earlier that objects in Java do not support operators like plus and minus. Generally, these types of operations would make no sense. However, here are some objects (possibly a complex number object) for which it would be handy to have the option. In other languages, it's common to use operator overloading to define the meaning of operations on certain objects. By assigning some meaning to an operator, you can then use standard operators, like the plus operator, to perform operations on objects. But the authors of Java felt this made code harder to read and maintain, so they didn't include it. Their decision on this matter is somewhat controversial.
The most common use for operator overloading is with strings. So as a compromise, the Java authors overloaded the plus operator to include strings. If either operand of the plus operator is a string, the result will be the concatenation of both. If one of the operands is not a string, it will be converted to one. The string created from the operand follows the rules listed in Table 4-12.
Operand | Rule |
---|---|
Null Variables | Any variable whose value is null will result in a string of "null". |
Integer | An integer will be converted to a string representing its decimal notation, preceded by a - sign if negative. There will be no leading zeros, and if the value is 0, the string 0 will be returned. |
Floating Point | Floating point numbers will be converted to a string in a compact fashion. If it exceeds 10 characters in length, it will be represented in exponential form. If it is negative, it will be preceded by a - sign. |
Character | A character will be converted to an equivalent string of length one. |
Boolean | The result will either be "true" or "false" based on the boolean's value |
Objects | An objects toString() method will be called. |
Once both operands are strings, the two will be concatenated. Let's look at a few examples that will illustrate this point:
String foo = "Hello "; String bar = "World"; int i = 42; boolean cont = false; String result = null; result = foo + bar; // = "Hello World" result = foo + i; // = "Hello 42" result = foo + cont; // = "Hello false"
Adding the plus operator seems like a good move-for strings, it makes a lot of sense. But now let's ask ourselves a question: If they changed the plus operator, then what does the minus operator do? The answer is nothing. And what do the == and != operators do? Stumped? Try this piece of code:
String foo = "Hello"; String bar = "Hello"; if (foo == bar) System.out.println("Equal"); else System.out.println("Not Equal");
The code above will output equal. It makes sense, doesn't it? The two strings are equal, so the equals operator returns true. Let's recall how the equals operator for objects works. It checks to see whether two objects are in the same place in memory not if their components are equal. Here is another piece of code using the equals operator:
class testString { String st = "Hello"; } class testString2 { String st = "Hello"; String st2 = "Hello"; public static void main(String args[]) { testString test = new testString(); testString2 test2 = new testString2(); if (test.st == test2.st2) System.out.println("Equal"); else System.out.println("Not Equal"); if (test.st == test2.st) System.out.println("Equal"); else System.out.println("Not Equal"); } }
The output of this code might be bewildering. In the first case, it will return Equal; in the second, it will return Not Equal. This is because internally, the compiler has performed a space optimization. The variables st and st2 have only been allocated one instance between them. Again, it makes sense, since they're the same value. The problem is that this masks how the == operator really works.
You can't use the == operator to compare two strings. You must use the equals method of the string class. Using the class definitions from above, we could rewrite the main method correctly as follows:
public static void main(String args[]) { testString test = new testString(); testString2 test2 = new testString2(); if (test.st.equals(test2.st2)) System.out.println("Equal"); else System.out.println("Not Equal"); if (test.st.equals(test2.st)) System.out.println("Equal"); else System.out.println("Not Equal"); } }
For further discussion of the String class, see "Strings" in this chapter and Chapter 6, "Discovering the Application Programming Interface."
A package is an organizational tool provided with the Java language. Conceptually a package is a grouping of related classes and interfaces. You are already familiar with a package, namely, java.lang. This package provides most of the functionality of the Java language. The Application Programming Interface (API) classes are grouped together as packages. They comprise a powerful tool that will allow you to make your own code libraries.
A package is stored in one or more source files. Each source file needs to have the package declaration at the top. Only one public class may be placed in each source file. When the source files are compiled, the class files will be placed in directories based on the package name, which is simply the directory path, with dots instead of slashes. Thus, if we wanted to create a package in the directory ventana/awt/shapes, we would use the following package command at the top of each source file:
package ventana.awt.shapes;
Packages are important for implementing code libraries, and they will be covered in greater detail in Chapter 10, "Advanced Program Design."
Once we have a package, how do we go about accessing its classes and interfaces? One method is to use a class' full name. Let's say we have implemented the shapes package above and it contains two classes, a circle and a rectangle. If we want to create a new instance of the circle class, we can use the following code: ventana.awt.shapes.circle circ = new ventana.awt.shapes(); Accessing classes using their full names can be tedious. Java's import statement makes it easier. When you import a package, you gain a shorthand convention for its classes and interfaces. Let's import the circle class from the shapes package and see what we have gained:
import ventana.awt.shapes.circle; class tryShapes { public static void main(String args[]) { circle circ = new circle(); } }
This option is significantly easier and requires much less typing. This is how you will access all the code that Java provides you. First you import the packages you want to use, and then you use the short form for the class and interface names.
There is one other shortcut available for dealing with classes. If you're working with a package that has many classes and interfaces, it would be a drag to have to list each class you wanted to use. You can use a form of wildcards in your import statement by specifying, at the end of the statement, an asterisk instead of a class or interface name. This says to import all classes and interfaces into that package. To access all the members of the shapes package, we would use an import statement like this:
import ventana.awt.shapes.*;
Don't worry about this increasing your code size. It only loads the package's contents into the compiler's symbol table, which is basically a big dictionary the compiler uses to look up references in your program. You can use the long form or the short form. Both are correct, so the choice is yours.
We have declared classes in the previous chapters. Now we will cover the nuts and bolts of the syntax. This discussion will introduce you to new material and serve as a reference as you progress to more advanced topics. If the concept of objects is new to you, please read Chapter 3, "Object Orientation in Java." Without an understanding of objects, the rest of this book will make little sense.
The class is the fundamental building block for Java programs. It is composed of data and methods. The methods define ways to modify and interact with the encapsulated data. By making one unit contain both the data and the ways to modify it, we increase our code's reusability and make maintenance much easier.
In Java, a constructor must be called in order to create a new instance of an object. The constructor for a class is a method with the name of the class and no return type. You can have multiple constructors, but they must each have a unique signature, consisting of the number and type of parameters. The names of the parameters make no difference in the constructor's signature. Having two constructors with the same parameter (a String, for instance), but with different names, is not valid. Let's look at a few constructor definitions:
class foo { foo() {...} // No parameter constructor foo(int n) {...} // Takes one int parameter foo(String s) {...} // Take a string parameter }
The constructors that each take one parameter are unique because the types are different. You could not have another constructor like foo(int i)- although the name is different, the type is not unique. The types or number of parameters must be unique.
Each class may have one destructor . The destructor is called when the object is slated for garbage collection, so you can't be sure when the destructor will be called. This is a good place for closing files and releasing network resources. You wouldn't want to get user input or interact with other objects in this code.
The destructor in Java is called finalize . It has no return type and takes no parameters. We could add a destructor to our class foo with this code:
class foo { finalize() {...} // Do some cleanup code }
Class declarations can be modified in three different ways-they can be declared abstract, final, or public. The modifiers go before the class keyword. We can specify a class foo with a class modifier in this way:
public final class foo {...}
A public class can be accessed from other packages. If a class is not public, it can only be accessed by the package in which it is declared. You can only declare one public class per package. Therefore, a source file can only contain one public class or interface.
The final modifier signifies that the class can't be extended. Some of the classes in the Java API are defined final. For example, the Array and String classes are final because they are hybrid classes. In other words, they are not fully objects, but have support code directly in the compiler. Generally, you want to avoid making a class final because then your class cannot be subclassed. This is not good object-oriented programming and means people will not be able to inherit your functionality. The final modifier is useful when dealing with native methods or other nonportable activities.
By declaring a class abstract, you are telling the compiler that one or more methods will be abstract. An abstract method is one that has no code and will be implemented in later subclasses. It can't be instantiated, but it can be extended. Each subclass of the abstract class must instantiate the abstract methods or be declared abstract themselves. This is useful for defining concepts and then allowing specific implementations to follow. A mechanism for declaring a whole class as abstract is called an interface . For more on interfaces, check out the "Interfaces" section later in this chapter, and see Chapter 3, "Object Orientation in Java."
The Extends Keyword
Inheritance relationships are created with the extends keyword. A class may extend, at most, one class. Hence, multiple inheritance is not explicitly supported. Through the use of interfaces, however, it's possible to mimic some features of multiple inheritance.
All objects in Java have the same parent object called Object. If you don't specify a parent object for a class, it will descend from the class Object by default. To specify a parent for a class, use the extends keyword. If we had already created a class foo, we could create a subclass bar by:
class bar extends foo {...}
A subclass inherits the methods and variables of a class. You can redefine or shadow a method or variable by creating an identifier with the same name. In order to access the shadowed identifiers, you can use the super variable. This variable points to a class's immediate parent. If foo had a method called test, and we shadowed it in bar we would have to use this mechanism:
class bar extends foo { void test() { super.test(); // Call parents test(foo.test) // Do more work } }
When extending a class, you will get a compile-time error if you
make a circular inheritance relationship. Class B cannot be a
subclass of A if you have already made A a subclass of B.
Implements
A class can implement any number of interfaces. An interface is a class whose methods are all abstract. The implements keyword is the final component of a class declaration. The complete syntax for a class declaration is:
ClassModifiers class ClassName extends ParentName implements InterfaceName {...}
Everything but the class keyword and ClassName is optional. If a class implements an interface, it needs to provide code for the methods defined in the interface. The only exception to this rule is, if the class is abstract, its "children" are responsible for implementing the interface's methods.
If we had an interface called shapeInterface that had two methods, draw and erase, then we could define a class called shape that implements this interface:
class shape implements shapeInterface { void draw() {...} void erase() {...} }
You can have a class implement multiple interfaces by separating the interfaces with commas. The programmer then must implement all the methods of each interface. If we had two interfaces, say shapeInterface and moveableInterface, then we could define a class, dragDrop, that implements both of these interfaces:
class dragDrop implements shapeInterface, moveableInterface {...}
For a conceptual discussion of interfaces, see Chapter 3, "Object Orientation in Java." The syntax for interfaces can be found later in this chapter in the section, "Interfaces."
When defining variables for a class, you can specify certain modifiers. These modifiers affect such things as which classes can access the variables, certain multithreading operations, and whether a variable is static or final. The variable modifiers are public, private, protected, static, final, transient, and volatile.
A variable can be declared public, protected, private protected, or private. A public variable can be accessed in the package in which it was declared and by any other packages. It is the least restrictive access modifier.
A protected variable in a class C is available to classes that are in the same package or that are subclasses of class C. This restricts classes outside of the package from accessing the variable, but it allows any subclasses of the class to access it.
If a variable in class C is defined as private protected , it can be accessed only from a subclass of C. It can't be accessed by other classes in the same package. So if no other classes in the package need access to a variable, this is the appropriate access modifier to use.
A private variable is only accessible by methods of the class in which it is defined. This is the most restrictive access modifier. Subclasses of the class cannot access the private variable.
Here is an example using all four of the access modifiers:
class circle { public String className; protected int x,y; private protected float radius; private int graphicsID; }
If a variable is declared static, there is only one variable of that name for all instances of the object. The variable is allocated at compile-time, so you don't have to instantiate the class to access the variable. This is how the Math class in the java.lang package provides its constant variable PI. We can print this value like so:
System.out.println("PI:" + Math.PI);
The final modifier states that the variabl's value cannot be changed. It must contain a variable initializer, and any attempt to modify the variable will result in a compile-time error. The final modifier is usually used to specify a constant. Constants are normally specified as public static final. We could declare a const for some class foo as:
class foo { public static final int Answer = 42; }
The transient and volatile modifiers are part of the multithreading modifications to the language. They are primarily used for optimization purposes. A variable that is declared transient is not part of the persistent state of the object. The transient keyword will be used to implement some functions in later versions of the Java language.
A volatile variable is a variable that is known to be modified asynchronously. Volatile variables are reloaded and stored to memory after every use. These two modifiers are reserved for future use, but they are currently valid modifiers (for more on volatile variables, see Chapter 11, "Advanced Threading."
A method can be modified with the modifiers listed in Table 4-13. The public, protected, and private keywords function like the variable modifiers. They determine which classes can access the methods.
public | protected | private | static |
abstract | final | native | synchronized |
The static modifier makes a method accessible even when the class isn't instantiated. A static method is implicitly declared final; therefore, you cannot override a static method. Inside a static method, you can access only members of the class that are also static.
An abstract method is one that will be implemented in a later subclass. If you make any method within a class abstract, the class is abstract and can't be instantiated. If all the methods of a class are to be abstract, you might consider defining the class as an interface.
A final method is one that cannot be overridden by a subclass. A private method is effectively a final method because it can't be overridden. An optimizing compiler might "inline" the method to increase speed. To "inline" a method is to copy the code to each reference in your program. This trades code space for speed and is a common practice in C++.
The synchronized keyword is used to mark a method as needing the class's monitor lock before it can be executed. We discuss synchronization and multithreading in Chapter 11, "Advanced Threading."
If you wish to write code that handles various parameter types under the same name, you can overload a method. Usually, you overload a class's constructor so that it will accept many kinds of initialization information. You can overload almost any method, be it a constructor or a normal method. However, you cannot overload the clas's destructor, since it takes no parameters and you can't control when or how it is called.
To overload a method, make another method, using the same name and return type but different parameters. Each overloaded method must be unique, and uniqueness is determined by the number of parameters and their types. Parameter names do not affect uniqueness. The following code will not compile:
class foo { foo(int i) {...} // will not compile foo(int j) {...} // Same number & type }
The code above tried to have two methods that both take an int as their parameter. You can't have two overloaded methods that have the same number and type of parameters.
One important note about overloading-you need to know what type you are passing into a method. Consider the two methods declared below:
class foo { foo(int i) {} foo(byte j) {} }
This code will compile, but look closely at what you have specified. If you pass a short or long into this method, it won't compile. Java won't perform any type conversions on overloaded methods. As we noted earlier the lack of explicit type conversion is an important distinction between Java and C/C++.
Reference Variables Type Conversion
All references to objects and interfaces are done through reference variables , otherwise known as pointers. You may have heard that Java has no pointers, but that's not technically true. Java has no pointer arithmetic , but it still has pointers. In order to make Java a safer and more robust language, its designers decided that the only way to change where a reference variable points would be through an assignment statement.
It's easy to define a reference variable. Use the class or interface name you want to contain, but don't call the new operator. You can then assign this variable to an object or interface that has already been created. Let's define a reference variable to hold a class called myClass and one to hold an interface called shapes:
myClass myRef; shapes myShapes;
There are rules for assigning values to a reference variable. You can always assign a reference variable to other references of the same type. A question arises when you try to assign it to another type, an interface, or an array. In some cases you can do it, in other cases you will need to type cast it, and in still others, it's just not possible. The rules for when Java will automatically convert it for you are somewhat complex. We will give you some guidance, but the Java Language Specification will have to be your ultimate authority on the subject.
If you create a reference variable of class S, you can assign the following types to it:
If you create a reference variable of interface S, any assignments to S must implement S. Type consistency must be determined at runtime because a subclass of S might have implemented the interface.
It might be possible to use casts for other assignments. If you use a cast to assign values to reference variables, you may generate runtime errors. Make sure you know what this object will be at runtime; if you're not sure, be certain to catch the CastClassException.
An interface is equivalent to a class with all its methods declared abstract and its variables declared static and final. (For definitions of these terms, see "Variables Modifiers" in this chapter.) Every method is left unimplemented, and all variables are constants. Every variable within an interface must contain an initializer, and the variable may not be declared as transient or volatile. An interface's methods may not have the modifiers of final, native, static, or synchronized.
Any class that implements the interface will be responsible for coding the methods. An interface is primarily a conceptual model. It is helpful in the design of object hierarchies. Some code design methodologies design all of the interfaces and classes first, which helps prevent code integration problems. Later, the interfaces are implemented. Designs made in this manner are usually more general and can be more easily extended.
Defining an interface is very similar to defining a class. The major difference is use or the interface keyword instead of class. We can define an interface called shapeInterface that has two methods, draw and erase, as follows:
public interface shapeInterface { pubtic void draw(); public void erase(); }
We can now implement several different shapes that use this interface. Later, we'll be able to create data structures that contain variables of the type, shapeInterface. Regardless of the actual shape, we can access the interface's methods. The interface is a powerful design tool, and is especially useful for making code reusable and simpler to understand.
An array is a hybrid primitive-object type. It looks like an object, but it has special meaning to the compiler. The Array class is declared final, so you will not be able to extend its functionality in an object of your own.
An array is commonly used to store a group of similar information. All items in an array must be of the same compile-time type. If the array is made up of primitive types, they must all be of the same type. If it consists of reference types, they must all point to a similar type.
An array is like an object in that you must use the new operator to instantiate each element. In this respect, Java arrays differ from the arrays implemented in most languages. In most languages, each element of the array is already initialized for you.
Arrays in Java are significantly different from arrays in most
languages. It will save you time in the long run to learn a little
about them now. Don't assume that they act the same as arrays do
in C or Pascal. If you have played with SmalITalk, then you are
more familiar with Java-style arrays.
Arrays are initialized using the new operator. Think of each element in the array as a separate object. The array itself can be thought of as a container for all the objects, which gives them a common access point.
The simplest type of array is a one-dimensional array of a primitive type-for instance, an int. The code to create and initialize this array is:
int nums[] = new int[5];
Looking at this declaration, we can see most of the concepts important to declaring arrays. The brackets after the identifier, nums, tell the compiler that nums is an array. The new operator instantiates the new array and calls a constructor for each element. The constructor is of type int, and it can hold five elements. If you keep in mind that you can use constructors to create arrays, you'll have no problem with multidimensional arrays.
Arrays in Java must have at least one dimension specified. The rest can be determined at runtime. To create and initialize a two-dimensional array we can use the following code:
class tarray { public static void main(String args[]) { int numsList[][] = new int[2][]; // Specify second dimension later numsList = new int[2][10]; for(int i=0; i < 10 ;i++) { numsList[0][i] = i; numsList[1][i] = i; } for(int i=0; i < 10; i++) { System.out.print(numsList[0][i] + " "); System.out.println(numsList[1][i]); } } }
This code creates an array called numsList, in which only one dimension is specified. We can then specify the second dimension. It's important to remember to call a constructor for each element of the array. We can use this feature of the language to create nonrectangular arrays. Take, for instance, the creation of a triangular array:
int[][] createArray(int n) { int[][] nums = new int[n][]; for(i=0; i < n; i++) { nums[i] = new int[i+1]; } return nums; }
This code will create a triangular array of n elements. The first element will have one element, the second will have two elements, and the last will have n elements. This type of initialization creates many new applications for arrays. Where once a linked list or other dynamic data structure was needed, we can now use an array.
Arrays can be initialized at the time of creation by enclosing the desired initial values within braces {}. You need not specify a size Java will initialize the array to the number of elements specified. You can nest the initializers in order to initialize a multidimensional array.
Let's look at an easy example-a one-dimensional array. We want an array that contains the numbers from 1 to 5. We can initialize it in this manner:
int nums[] = {1,2,3,4,5};
Initializing a two-dimensional array requires nesting the initializers. We can create an array of arrays that contain an integer pair:
int nums[][] = {{1,1},{2,2},{3,3},{4,4},{5,5}};
As you've just seen, initializing one- and two-dimensional arrays is easy. You may find it harder to picture the multidimensional array. The syntax is the same; you just need to nest more levels of initializers. In practice, you will end up using loops to initialize multidimensional arrays.
An array can be indexed by a byte, short, int, or char value. You cannot index arrays with a long, floating point, or boolean value. If you need to use one of these types, you will have to do an explicit conversion.
Arrays are indexed from zero to the length of the array minus one. You can determine the length of any array by looking at its length variable. We can use this variable to traverse an otherwise unknown length array:
long sum(int[] list) { long result=0; for(int i=0; i < list.length; i++) { result = result + list[i]; } return result; }
This code will sum an arbitrary length array of integers. It uses the length variable to determine the upper bounds for the array.
Java provides bounds checking. This means that each access to an array will be checked to make sure it is inside the array. If the index falls outside the array, an ArrayIndexOutOfBoundsException will be generated.
Arrays in Java are powerful, but you will pay for some of this power. Some compilers may optimize for rectangular arrays; others may not. Generally any array that uses a different size for each element will have a slower access time. Again, you must weigh the benefits of a simple implementation with the loss of speed.
Most computer programs make decisions that affect their flow. The statements that make these decisions are called control statements . Among these are the familiar if-then statements and looping statements. All computer languages have control statements, and many of these should be familiar to you. We will cover each one in detail. If you're not a C programmer, you may find the switch statement a worthy addition to your repertoire. Before we go any further, let's summarize the control statements available in Java:
if (boolean) statement1; else statement2; for(expression; boolean; expression) statement; while(boolean) statement; do statement; while(boolean); break label; continue label; return expression; switch(expression) { case value : statement; .... default : statement; }
The most commonly known control statement is the if else statement. An expression is evaluated to generate a boolean result of true or false. A result of true will cause the first statement to be executed; false will cause the else portion to be executed. The else portion is optional: if you don't want to perform any action when the comparison is false, just leave off the else statement. The following is a simple example of the if-else statement:
if (done == true) System.out.println("Done"); else System.out.println("Continuing");
You can also string multiple if-else statements together. Each if statement will be evaluated until one is true. If none of the if statements is true, then the else statement will be executed. We might use this feature to print different messages based on an integer representing the current temperature in Celsius:
int temp; if (temp < 0) System.out.println("Brr, it's freezing out"); else if (temp > 100) System.out.println("Water boiling?"); else System.out.println("Nice day isn't it!");
You may have noticed that only one statement is executed after an if statement. What if you needed to execute multiple statements? Java supports the concept of blocks. A block is a section of code that can be placed anywhere a statement is allowed. It is delineated by braces {}. Let's use this concept in another if-else example:
int itemCount; boolean checkout; if (itemCount <= 10) { System.out.println("Thank you, starting checkout"); checkout = true; } else { System.out.println("Maximum 10 items in express lane!"); checkout = false; }
In Pascal, blocks are delineated by the begin/end pair. Java has
adopted the C/C++ convention of using curly braces.
You need to be aware of one more issue concerning if-else statements-the problem of else statement ambiguities. A question sometimes arises as to which if an else statement belongs to. The following code exhibits this quality:
int checkCost, moneySaved; boolean overDraft; if (checkCost > moneySaved) // incorrectly coded if (overDraft == true) System.out.println("Overdraft enabled"); else System.out.println("Item purchased");
The example shown above, a simple check processing system, is incorrectly coded. Notice the else statement. It is indented as if it is to be part of the main loop. Unfortunately, this is not how the code will run. It's hard to tell which if statement the else is paired with. Else blocks are always associated with the last if block. If this is not what you want, you should enclose your code into separate blocks; doing so will ensure that the compiler and you know what will be executed. When it's rewritten to use blocks, the code looks like this:
int checkCost, moneySaved; boolean overDraft; if (checkCost > moneySaved) { if (overDraft == true) { System.out.println("Overdraft enabled"); } } else System.out.println("Item purchased");
Proper use of blocks will make your code easier to read and possibly stop you from making subtle logic bugs. The if-else statement is a useful tool in programming, but you must be careful to clearly pair all if-else pairs.
The while and do-while statements are used for two special cases of looping. The while statement is used when you may not want the loop body to be executed. The comparison expression is evaluated before the loop is ever executed. When this expression is false, the loop is exited. You must change the value of the loop variable inside the loop body; failure to do so will result in an endless loop. As is often the case, we have set a boolean variable someplace in our program, and we now wish to execute a piece of code based on its result. Let's assume we have a function named result, which returns a boolean value telling us whether we should continue:
while( result() ) { // Execute some statements .... }
This loop will execute until the function result returns false. If it initially returns false, the loop will never be executed. If we want the loop to execute at least once, we can use a do-while loop. This type of loop will execute once, regardless of its comparison expression. Using the same boolean function, result, we can rewrite the above code to execute a minimum of once:
do { // Execute some statements .... } while( result() );
The while and do-while statements are common loop constructs. Java also supports a loop type called a for loop. This new loop type doesn't add any new functionality but it can make your code easier to read.
The for statement is a rather powerful looping device. It provides an expression to initialize variables, followed by a comparison expression and then a place to increment or decrement the loop variables. This type of loop is particularly good for counting applications. With the following code, we will print the numeric values 0 to 4:
for(int i=0; i < 5; i++) { System.out.println(i); }
The three expressions of the for loop are evaluated at different times. The first expression, or initialization segment, is executed once at the beginning of the loop. The second, or looping, expression is evaluated before each iteration of the loop, including the first time. The last expression, the stepping expression, is executed after the completion of the loop body; this part is typically used to increment or decrement a variable.
There are a few points to be made about the program above. Notice that the initial expression creates a new variable. This is a legal and useful way to create temporary variables. By declaring the variable in the initialization expression, you scope the variable to its lowest level. Declaring a variable close to its use makes its function clearer to another reader. It also makes a portion of code more contained and easier to move around.
The second expression is the comparison expression. The result of this expression must be a boolean value. Unlike with C/C++, you can't have an expression that evaluates to an integer. A common shorthand among C programmers was an expression such as (i), which would evaluate to false when i was equal to zero; and true otherwise. But it won't work in Java; you'd have to write the expression as (i != 0). The authors of Java are trying to enforce good programming practices by making everyone's code a little easier to read.
All looping expressions must evaluate to a boolean expression. An
expression of an integer type cannot be a comparison expression.
java will not convert an integer to a boolean value.
The last part of a for statement is the looping expression, and it is executed at the end of each loop. Normally, it is used to either increment or decrement the loop variable. Stating each expression at the declaration of the loop makes it easier to understand how many times a loop will execute. The for loop can be made to mimic any other looping construct. To illustrate this, let's create the while loop:
boolean cont = true; .... for(; cont == true;) { // Statements that do some work ... // Logic that sets cont ... }
A while statement has a comparison statement, which is checked at the beginning of the loop. If the comparison is false, the loop never executes. By having no initializing or looping expressions, the for loop mimics the while loop. Notice that two semicolons are still used in the for loop. Even though there is no initializing or looping expression, you must provide a place for them.
Java has done away with the much-derided goto statement. Gotos, used mainly to exit loops on some exceptional condition, have largely been abandoned as being needlessly complex. Suppose you have a loop that is usually supposed to execute for 10 repetitions, but on alternate Thursdays it should execute only four times. Should you slip a goto statement in to exit the loop early? Nope-use Java's glorified gotos, called jump statements.
Java handles this situation with the break and continue statements. You can use these statements to exit a loop or method before it would normally exit. The type of statement you employ determines where control is transferred.
The break statement is used to transfer control to the end of a looping construct (for, do, while, or switch). The loop will exit regardless of its comparison value, and the next statement after the loop will execute. We might use a break statement to exit a while loop, which would normally run forever:
int i=0; while(true) { System.out.println(i); i++; if (i > 10) break; }
The above code would print the numbers 0 to 10. Normally, this loop would execute forever because the while statement always evaluates to true. The break statement directs the computer to exit the loop when i > 10.
The continue statement is similar to the break statement. The continue statement causes program execution to continue after the last statement in the loop. This is useful when you want to skip some steps of an execution body We can also use it to avoid a division by zero error:
for(int i = -10; i < 10; i++) if (i == 0) continue; System.out.println(1/i); // Control is transferred to here }
The loop will not process the case where i = 0 when the continue statement is used. In the example above, it's important to note that the looping expression, i++, is executed after the continue. If this were not true, the loop would get stuck. Continue statements are often used to skip sections of code that won't work for a particular value.
Both the break and continue statements can use optional labels. In fact, any statement in a Java program can be labeled. To add a label, use the label name, a colon, and then the statement. We could label a loop as follows:
loop: for(int i=0; i < 10; i++) { }
The application of labeled loops may not be apparent until you use multiple levels of loops. Imagine a situation in which you are buried several levels deep, and an error occurs. You might then wonder "Why doesn't Java have a goto statement?" The answer is that the goto statement is not needed. Java handles this situation with two mechanisms: exceptions, which we cover later in this chapter, and labeled jump statements.
A labeled jump statement is similar to the goto statement. Take a possible multiple loop situation: You are searching a two-dimensional array for a certain value, say 5. When you find that value, you wish to exit. Labeled jumps allow you to do that:
int i=-1,j=-1; int nums[][] = new int[5][5]; boolean found = false; loop: for(i=0; i < 5; i++) { for(j=0; j < 5; j++) { if (nums[i][j] == 5) { found = true; break loop; } } } if (!found) System.out.println("Value not found"); else System.out.println("Value found at " + i + "," + j);
This piece of code covers a fair bit of what we have learned in this chapter. It also represents a rather common task. If you are comfortable with this code example, you have a good grasp of such concepts as loops, arrays, operators, and jump statements. If you're new to languages such as C, these programs may be hard to read. C programmers should feel very comfortable with most Java constructs.
Return is a statement, like break and continue, that is used to transfer control. Instead of exiting loops, this statement exits methods. It can also be used to return information, hence its name. A return statement ends execution of a method and returns to where the method was called. Or you can provide a return value, such as an error code or some useful value-perhaps a number raised to some power. We could code a "to the power of N" function as follows:
class power { public int toN(int base, int n) { int result=base; for(int i=0; i < n - 1; i++) result = result * base; } return result; // No code after this will be executed } }
This code calculates base raised to the nth power. It then uses the return statement to deliver the result. Any code following the return statement is not executed. The value you return must be of the same type as the return type of the method. If the method has a void return type, you can just use the keyword return followed by a semicolon. The return statement doubles as a function-level goto and a way to return values.
In older languages, it was common to see large expanses of if-else statements. You may have seen one of these in a menu application. Typically the user enters a command, and then some code is executed based on the entered value. In large programs, especially programs like word processors, there might be hundreds of possible commands. The switch statement was developed to ease this burden.
A switch statement accepts a char, byte, short, or int as its comparison expression. This value is then looked up in the case statements that follow the switch statement, and the proper code is executed. Let's use our menu example to show a switch statement:
int cmd; // Assume the user has entered a command switch(cmd) i { case 1 : System.out.println("Menu item 1"); break; case 2 : System.out.println("Menu item 2"); break; default : System.out.println("Invalid command"); }
Each option you want to handle is specified in a case label. The value after the case statement must be of the same type as the switch expression. You can also have a label called default, which is executed if the switch expression is not found.
Notice the use of the break statement. This makes each case distinct. If there is no jump statement-which is commonly a break statement-the execution will continue into other cases. Sometimes this is desired, but other times it's not. You can use this to your advantage by grouping commands together that have similar operations. Just be sure to put in break statements when needed.
The Java language was designed around the C/C++ languages because they are solid languages and have a large user base. Some changes were made to enhance security, reduce bugs, and make maintenance easier but generally the languages were left alone. Java has been described by its authors as C++ without the weaknesses regarding safety and maintenance, but with the benefits of threading and exceptions. Because Java is made to work in a networking environment, it needs certain enhancements to gracefully handle a multitude of errors and complications.
Exception handling is required to deal with problems commonly found in networking. Many Net citizens know how frequently a connection can be lost. Your program must be ready to deal with this and other errors at any point in your code. One can imagine having a function that checks the state of the Net connection after every statement. Ludicrous as this sounds, early BBS software had something very similar. Good exception handling is almost impossible without language support, and Java provides this support.
Exceptions are thrown whenever the runtime system doesn't know how to handle a situation. Some common problems are runing out of memory dividing by zero, and accessing null pointers. If you have provided no exception handlers, then the runtime system grinds to a halt. Sometimes you have no way to fix the problem; other times you can fix the problem and continue on with the program. At a minimum, you should try to save any user data and close any files you have open. Either way, Java tries to make you more aware of possible problems. By using exception handling, you can create more robust programs.
The Java designers implemented exception support through the mechanisms of the try and catch statements. These statements specify blocks of code which direct how an exception will be handled. Basically you specify a block of code to try and then catch any exceptions that are generated. Consider the simple problem of dividing by zero:
int i = 0; i = i / i;
Upon execution of this code, the Java runtime system will generate an exception. The program will end, and an error message will be printed to the screen. Your paying clients will be giving you a call, inconveniently just as you sit down to dinner. You could have easily avoided this situation. Let's use the try and catch mechanism to catch this arithmetic exception:
int i = 0; try { i = i / i; } catch (ArithmeticException e) { System.out.println("Caught divide by zero, continuing"); }
After a try block, you can specify any number of catch blocks. When an exception is thrown the runtime system creates an object describing the error. These objects are descended from a class called Exception. The Exception objects are then passed on to our exception handlers. If there is no exception handler the system grinds to a halt. We have caught the arithmetic exception here. But other exceptions would still cause a runtime exception to be generated.
When an exception is generated, the computer looks for catch blocks that might handle it. It looks for the best match first. If it can't find an exact match, it then travels up the inheritance tree for other possible matches. Since the arithmetic exception is a subclass of Exception, it would be picked. This concept is easier to understand with an example. The previous example can be rewritten to handle all errors:
int i = 0; try { i = i / i; } catch (ArithmeticException e) { System.out.println("Caught divide by zero, continuing"); } catch (Exception e) { System.out.println("Caught some exception"); // Perform some cleanup code ... // Regenerate the exception throw(e); }
You may notice a problem in the code above. We have caught an exception, but we don't really know which one we caught. If you don't know how to handle an exception, it's best to let the runtime system handle it. Here, we caught some exception, did the cleanup work we could do, and then passed the error on. If no other error handlers are present, the program will stop-unless somewhere higher up in the code, we have created more error handlers. It's best to handle exceptions as close to the source as possible, but you can handle some errors in very general ways. This code would then be written somewhere else, possibly in the main loop of your program.
The throw command takes an object and passes it up the exception chain. You can have multiple levels of try and catch statements. A throw statement ignores the current level of catch statements and tries to find a match. Again, if no match exists, it will generate a runtime exception.
Sometimes you need to execute a piece of code even if an exception is generated. This can be accomplished using a finally block. No matter how you leave the block-as you normally do, as an exception, or as a jump statement like break or continue-the finally block will be executed. A skeleton try, catch, finally block would look like this:
try { // Try some code that might generate an exception } finally { // Perform any cleanup needed } catch (ArithmeticException e) { // Handle a divide by zero exception }
The previous skeleton program provides a good framework for dealing with pieces of code that can generate exceptions. Enclose the suspect code in a try block. Perform any cleanup needed in the finally block. Handle any errors that you can safely diffuse, and allow the rest to be passed up the exception chain. Now you have created a well-behaved system that will handle the errors it can, always clean up after itself, and pass on exceptions it can't handle.
Table 4-14 contains a list of common exceptions generated by the runtime system. Most classes in the API will also have their own exceptions. When using a new class, make sure you familiarize yourself with the exceptions it can generate.
Exception | Description |
---|---|
ArithmeticException | An exceptional arithmetic condition has occurred; for example, trying divide a number by zero. |
ArrayIndexOutOfBoundsException | You have an index value that is outside the bounds of the array. Remember that array start at 0 and go to N-1 where N is the number of elements of the array. |
ArrayStoreException | You have tried to store the wrong type of the object in an array. The object must be either same type, a subclass, or one that implements the interface specified. |
CastClassException | An invalid cast has occurred. |
InstantiationException | You have tried to instantiate an interface or an abstract class. |
NegativeArraySizeException | You have tried to create an array with a negative size. |
NullPointerException | You have tried to use a reference variable that has not been initialized or has been set to null. |
NumberFormatException | A value could not be converted to a number. This typically happens when trying to convert a number. |
OutOfMemoryError | Even after garbage collection, you have run out of memory. This happens infrequently, but it is possible. |
SecurityException | You have tried to do something that the security manager doesn't like. |
StringIndexOutOfBoundsException | While trying to access a string as an array, you specified an index value a outside the length of the string. |
We hope you'll refer back to this chapter whenever you have a syntax question while programming. Now that we've gotten most of the basics out of the way it's time for a little fun. In Chapter 5, "How Applets Work," we cover the essentials of programming interactive Java applets. You're about to see how to make your Web pages come alive.