In many ways, a background in Delphi programming is ideal for making the move to Java. Although Java is based on C++ and borrows much of its syntax from C++, in some respects Java is more similar to the Object Pascal language of Delphi. Although you will have to adjust to some new syntactic conventions (such as using curly braces instead of begin and end), the object-oriented programming model of Delphi has a great deal in common with Java. This chapter will take you through a whirlwind tour of Java, pointing out the key differences between Java and Object Pascal. On the way, you'll learn how to translate your Delphi knowledge to Java. You'll also be exposed to new concepts such as multiple inheritance. This chapter doesn't attempt to teach you everything you need to know to be a Java programmer, but it will help you use your Delphi experience as a first step toward Java.
In order to make the code samples in this chapter as understandable
as possible, it is useful to include comments. So, this chapter
starts right off with an overview of Java comments. Java supports
three comment delimiters, as shown in Table 5.1.
Start | End | Purpose |
/* | */ | The enclosed text is treated as a comment. |
// | (none) | The rest of the line is treated as a comment. |
/** | */ | The enclosed text is treated as a comment by the compiler but is used by JavaDoc to automatically generate documentation. |
If you've been exposed to C or C++ at all, you may already be familiar with the first two comment styles. The third is new to Java. The // comment delimiter is very useful when placed to the right of a line of code. The /* and */ delimiters are most frequently used to indicate that a block of lines is a comment. As examples of how these comment delimiters can be used, consider the following:
print("hello"); // this is a comment to the end of the line
/* this is a comment also */
/* this is a comment that
takes up multiple lines */
/** this is a comment that generates documentation.
It can be as many lines as desired. */
With Java, you will say good-bye to your old friend keywords begin and end. They are replaced by the more C-like curly braces, as shown in the following:
if (x == y) {
// do something
// do something else
}
As with Object Pascal, if a block is comprised of only a single line, then the curly braces are optional, as follows:
if (x == y)
// do something
if (x == y) {
// do something
}
Built into Delphi's Object Pascal language is a very broad set of data types. Many of Delphi's data types exist to provide compatibility with either prior versions of Borland's Turbo Pascal predecessor to Delphi or with Microsoft Windows. This section describes the Java primitive types and discusses the differences between them and their Delphi equivalents.
Variable declarations in Java are the complete opposite of what you are used to in Delphi. A Java declaration consists of the variable type followed by its name. For example, consider the following declarations:
int anInt;
int anotherInt = 42;
long thisIsALong = 100, secondLong;
float myFloat = 3.49f;
double myDouble = 3.1416D;
As you can see from these examples, more than one variable can be declared on a line. Additionally, an initial value can be assigned to a variable as was done, for example, by assigning 42 to anotherInt. In the case of float or double variables, the type of the variable can be specified by appending an upper- or lowercase F (for float) or D (for double) to the initial value.
The best news about declaring a variable in Java is that it does not need to be done in a special var section. Anywhere you need a new variable, you simply declare it. It can be at the start of a method or even in the middle of a method.
A language's primitive types are the building blocks from which
more complicated types (such as classes) are built. Java supports
a set of eight primitive types, which are shown in Table 5.2.
Type | Description |
byte | 8-bit signed integer |
short | 16-bit signed integer |
int | 32-bit signed integer |
long | 64-bit signed integer |
float | 32-bit floating-point number |
double | 64-bit floating-point number |
char | 16-bit Unicode characters |
boolean | Can hold true or false |
From Table 5.2, you can see that Java uses a shorter version of
the type names than does Delphi. For example, Delphi has shortint
and longint, whereas Java
has short and long.
However, behind these cosmetic changes are some substantive changes.
For example, each of the similarly named integer types in Delphi
is bigger in Java. A Delphi Integer
is a 16-bit signed value and corresponds to a Java short.
Table 5.3 is useful for converting between Delphi and Java integer
variables.
Delphi | Java |
ShortInt | byte |
Integer | short |
LongInt | int |
Byte | byte |
Word | int |
All variables in Java are signed. Because of this, the Delphi unsigned types (Word and Byte) are replaced with Java signed types large enough to hold their largest values. Because Java provides Unicode support, you should notice that its char type is 16 bits wide. This is also why the 8-bit byte type is included as a primitive.
The Java and Delphi boolean types are similar. However, with Delphi it is possible to perform some operations on Boolean values that are not possible in Java. For example, in Delphi the following comparison will evaluate to true, but will be a compile error in Java:
false < true
The Object Pascal language of Delphi also includes a String primitive type. Although Java does not include a String primitive, it does include a full-featured String class that will provide many of the same capabilities you are used to from Delphi's String primitive. An important difference between strings in Java and Delphi is that Java strings are enclosed in double quotes ("), not single quotes ('). In Java, a single character is enclosed within single quotes. For example, consider the following valid Java declarations:
String str = "This is a string";
char ch = 'a';
In both Java and Delphi, it is possible to cast a variable from one type to another. Both languages are strongly typed, but Java is more restrictive and doesn't allow some casts that Delphi does. For example, in Java you cannot cast between a boolean and a numeric type. This means you cannot do the following:
boolean aBool = false; // not legal in Java
int anInt = (int)aBool; // not legal in Java
Delphi assumes that a Boolean value false can be translated to zero and true can be translated to one. This is not true in Java. In Java, a Boolean is a Boolean and it cannot be cast into an integer value.
Related to casting is the concept of automatic coercion. Automatic coercion occurs when a compiler coerces, or casts, a variable of one type into another automatically. For example, consider the following Delphi code:
var
anInt : integer;
aLong : longint;
begin
aLong := 65536;
anInt := aLong;
MessageDlg('anInt is ' + IntToStr(anInt), mtInformation, [mbYes, mbNo], 0);
end;
In this example, the 65536 stored in aLong is also placed into anInt. Because no explicit cast is performed, an automatic coercion from a longint to an integer is performed. Unfortunately, this will result in an error because the value in aLong is too large to fit in anInt. The automatic coercion will place 0 into anInt instead of the desired 65536. Java avoids the problems caused by automatic coercion by not allowing it.
The Java language contains a richer set of operators than does
Delphi. Fortunately, many of the Java operators correspond to
similar operators in Delphi. Table 5.4 shows each of the Java
operators, the operation it performs, and an example of its use.
Operator | Operation | Example |
= | Assignment | a = b |
> | Greater than | a > b |
< | Less than | a < b |
<= | Less than or equal to | a <= b |
>= | Greater than or equal to | a >= b |
== | Equal to | a == b |
!= | Not equal to | a != b |
! | Boolean negation | !a |
~ | Bitwise logical negation | ~a |
?: | Conditional operators | a ? expr1 : expr2 |
&& | Conditional AND | a && b |
|| | Conditional OR | a || b |
++ | Increment | a++ or ++a |
-- | Decrement | a-- or --a |
+ | Addition | a + b |
- | Subtraction | a - b or -b |
* | Multiplication | a * b |
/ | Division | a / b |
% | Modulus | a % b |
& | Bitwise AND | a & b |
| | Bitwise OR | a | b |
^ | Bitwise XOR | a ^ b |
<< | Left shift | a << b |
>> | Right shift | a >> b |
>>> | Right shift with zero fill | a >>> b |
+= | Assign result of addition | a += b |
-= | Assign result of subtraction | a -= b |
*= | Assign result of multiplication | a *= b |
/= | Assign result of division | a /= b |
&= | Assign result of bitwise AND | a &= b |
|= | Assign result of bitwise OR | a |= b |
^= | Assign result of bitwise XOR | a ^= b |
%= | Assign result of modulus | a %= b |
<<= | Assign result of left shift | a <<= b |
>>= | Assign result of right shift | a >>= b |
>>= | Assign result of right shift with zero fill | a >>>= b |
One of the biggest adjustments you'll need to make will be getting used to using a lone equal sign for assignment instead of the := of Delphi. Because = is used for assignment, two equal signs (==) are used for testing equality in Java. You can see this in the following Java example:
int x, y; // declare two integer variables
x = 43; // assign a value to x
y = 42; // assign a different value to y
if (x == y) // do something
In Delphi, the four keywords not,
and, or,
and xor each had two meanings.
Each could function as a bitwise operator as well as a Boolean
operator. This had a tendency to make code more difficult to read
and maintain than it needed to be. Java simplifies this by having
each operator perform a unique operation. To find the Java equivalent
for the Delphi not, and,
or, and xor
operators, see Table 5.5.
Delphi | Java | Operation |
and | & | Bitwise AND |
or | | | Bitwise OR |
xor | ^ | Bitwise XOR |
not | ~ | Bitwise negation |
and | && | Conditional AND |
or | || | Conditional OR |
not | ! | Boolean negation |
Java includes increment and decrement operators that are extremely useful. In order to increment a variable, use the ++ operator. In order to decrement a variable, use the -- operator. These operators can be used in either prefix or postfix mode, as shown in the following examples:
x++;
--y;
int z = 100;
MyMethod(z++);
In the case of x, the code x++ is equivalent to the more verbose Delphi x := x+1 or Inc(x). The example of the call to MyMethod is illustrative because it shows a unique aspect of the increment and decrement operators. The increment of z will happen after the call to MyMethod. This means that z will be passed with a value of 100. Once MyMethod returns, z will be incremented. If you want to increment z prior to calling MyMethod, the method should have been invoked as follows:
int z = 100;
MyMethod(++z);
A special assignment operator sounds like something out of a 1960s spy movie. Unfortunately, a Java special assignment operator isn't anything that glamorous. However, special assignment operators are extremely useful.
In addition to the usual operators, Java includes a set of operators that perform an operation and then assign the result of that operation to one of the operands. Each of these operators is listed in Table 5.4 where you'll notice that each is composed of a different operator token with an equal sign (=) appended. Most likely, you will use the +=, -=, *=, and /= operators more frequently than the others, so consider the following examples:
int a, b, c, d;
a = 5;
b = 2;
a += 3; // a now equals 8
a *= b; // a now equals 16
a -= 4; // a now equals 12
a /= b; // a now equals 6
From this example, you can see that a += 3 is shorthand for the Delphi statement a := a + 3.
Just your luck. You've finally mastered pointers and then you decide to learn Java-a language that doesn't use pointers. Pointers can be a very powerful aspect of a language. Unfortunately, pointers are also frequently behind much of the complexity of the languages that use them.
The Object Pascal language of Delphi took a big step toward removing the complexity of pointers by hiding them in many cases. However, Java goes even further by not having a pointer type at all. When an object or array is passed to a method, it is passed by reference, rather than by value. However, a Java program still cannot access it as a pointer or memory location.
By removing pointers from Java, the developers have greatly simplified the language. However, an additional benefit is that removing pointers is consistent with the design goal of keeping Java a secure environment. By removing the ability to create a pointer directly into a system's memory, a language goes a long way toward preventing the use of the language for writing deviant programs such as viruses.
Although the Object Pascal language of Delphi is undeniably object-oriented, it just as undeniably shows its lineage as a non-object-oriented language. Because Object Pascal is a descendant of the Pascal language, it includes support for all of the traditional Pascal constructs, including record types. However, because Object Pascal allows for the definition of classes, records are superfluous but remain in the language to provide backward compatibility. Because Java is a new language with no need for backward compatibility, its object model is much cleaner. In Java, you define classes. There is no concept of a separate but similar record type.
Like Delphi, Java uses square brackets ([]) to declare and access arrays. However, when declaring an array in Java, there are three important differences from declaring an array in Delphi, as follows:
To see the effect of these differences, consider the following array declarations:
int intArray[];
float floatArray[];
double [] doubleArray;
char charArray[];
As you'll notice from these examples, the square brackets ([]) can be placed either before or after the variable name. There is no difference based on where the brackets are placed. It is largely a matter of personal preference whether you place brackets before or after a variable name. However, as you settle on a personal preference, you should be aware of the difference that bracket placement can make when declaring more than one array in a single statement. For example, consider the following array declarations:
int [] firstArray, secondArray;
int thirdArray[], fourthArray[];
Each of these lines declares two arrays. In the first case, the brackets are required only once because they follow the type name. In the second case, the brackets are required after each variable name. Some programmers prefer the explicitness of such declarations, and others prefer the succinctness of the first case.
Of course, in Delphi you must specify the dimension of the array by specifying low and high index values into the array. In Java, this is not necessary (or even allowed) because Java requires that all arrays be allocated with new. To allocate an array using new, you would use code similar to that shown in the following examples:
int intArray[] = new int[100];
float floatArray[];
floatArray = new float[100];
long [] longArray = new long[100];
double [][] doubleArray = new double[10][10];
From these examples, you can see that memory can be allocated on the same line on which the array is declared, as was done with intArray, or the array can be declared and allocated on two separate lines as with floatArray. The variable doubleArray shows how to declare and allocate a multidimensional array in Java. In this case, a two-dimensional array is allocated. This is really an array of arrays in which each of 10 first-dimension arrays contains its own array of ten items.
An alternative way of allocating a Java array is to specify a list of element initializers when the array is declared. This is done as follows:
int intArray[] = {1,2,3,4,5};
char [] charArray = {'a', 'b', 'c'};
String [] stringArray = {"A", "Four", "Element", "Array"};
In this case, intArray will be a five-element array holding the values 1 through 5. The three-element array charArray will hold the characters 'a', 'b', and 'c'. Finally, stringArray will hold the four strings shown.
The initial element in a Java array is stored in index zero. Therefore, a Java array consists of elements 0 through one less than the number of items in the array. You can access items in an array in the same way you do in Delphi, as shown in the following:
int ages[] = {16, 18, 21, 65};
int canDrive = ages[0]; // can drive at 16
int canVote = ages[1]; // can vote at 18
int canDrink = ages[2]; // can drink at 21
int canRetire = ages[3]; // can retire at 65
Like Delphi, Java protects you against attempting to access an element outside the bounds of the array. Java will throw the ArrayIndexOutOfBoundsException exception if you try to access an element beyond the bounds of the array.
Like in Delphi, an instance of a Java class is allocated with the keyword new, as follows:
Label myLabel = new Label("This is a label.");
Although there is a Delphi function called new, the Java version of new is more similar to the Create method (the constructor) that can exist for a Delphi class. In Delphi, new is used to allocate a block of memory and set a pointer to it. In Java, new is used to create an instance of a class.
In Delphi, an instance that has been allocated with the Create method must eventually be released by a call to the Free method. For some instances in a Delphi program, the call to Free is performed automatically when the owner of an object is freed.
Although memory for an object is still allocated with new, there is no corresponding free method that must be used to release memory. What happens instead is that the Java memory manager keeps track of which memory is in use, and once there are no objects referencing a particular area of memory, that memory is automatically released and available for reuse.
With automatic memory management (usually known as garbage collection), you will find that you can write programs more quickly and with fewer bugs.
Java's support for classes has a great deal in common with Delphi. Although classes are undeniably important in Delphi, they are mandatory and central to all that you will do in Java. In Java there are no freestanding variables or functions. Everything must be encapsulated within a class. Further, every class in Java can trace back through its inheritance hierarchy and find itself a descendant of the Object class, similar to the way every Delphi object is derived from TObject. In order to understand Java classes, consider the following class definition:
class Employee {
public String firstName;
public String lastName;
protected int age;
private float salary;
public boolean CanVote() {
return age >= 21;
}
public boolean CanDrink(int legalAge) {
return age >= legalAge;
}
}
You'll notice from the definition of Employee that Java classes support the familiar concepts of private, protected, and public members. However, in Java, members are not grouped into private, public, or protected sections as they are in Delphi. In the Employee class, each member has its access control modifier specified right with the type and name of the member. Although you may consider this a little more typing, it definitely makes the code more readable, and therefore easier to maintain, because you don't have to read backwards through a file to find out if a class member is accessible.
Each of the familiar private, protected, and public access control modifiers has the same meaning in Java that it has in Delphi. Because Java does not have a concept analogous to Delphi's Visual Component Library (VCL), there is no published access control modifier in Java. However, Java does have a fourth level of access control that is used as the default. If no access control modifier is specified for a member, that member is accessible throughout the package in which it is defined, but nowhere else.
A Java package is similar to a Delphi unit but without the structure of interface, implementation, and initialization sections. Java code is shared at the package level, and a package contains the definitions and source code implementations of one or more classes.
Returning to the definition of the Employee class, you can also see that both the CanVote and CanDrink methods were written directly in the class definition. This is different from Delphi where the method prototype is given in the interface section, and the code for the method is actually written in the implementation section. The Java approach of combining a method's interface and its implementation simplifies maintenance because there is no need to duplicate method prototypes between implementation and interface sections.
It's always the little things that make a big difference. One little thing that makes a tremendous convenience improvement in Java over Delphi is the ability to set a default value for a member variable at the time it is declared. For example, consider the following definition of the Employee class:
class Employee {
...
protected int age = 21;
...
}
In this example, the member variable age is declared and is given a default value of 21. In Delphi, this would have been done in the constructor. There are two advantages to setting a default value at the point where the variable is declared:
In addition to class methods and variables that are associated with each instance of a class, a Java class can contain members that are associated with the class itself. These are known as static members and are identified with the static keyword, as follows:
class Employee {
...
static double maxSalary = 1000000D;
...
}
In this example, the member variable maxSalary will exist once in the entire program, as opposed to once per instance of the class. Additionally, maxSalary has been set to an initial value of $1000000.
Each Java class you define may include one or more constructors. In Delphi, the constructor for a class is named Create and is identified by the constructor keyword, as follows:
constructor Create(Owner: TComponent); override;
In Java, a constructor is given the same name as the class. It is also not necessary to specify constructor and override as is done in Delphi. This can be seen in the following example:
class Employee {
public String firstName;
public String lastName;
protected int age;
private float salary;
public Employee(String fName, String lName) {
firstName = fName;
lastName = lName;
}
public boolean CanVote() {
return age >= 21;
}
public boolean CanDrink(int legalAge) {
return age >= legalAge;
}
}
In this example, the constructor is named Employee and is passed two String parameters-one for the employee's first name and one for his last name.
In Delphi, each class can have a destructor named Destroy. You call Destroy when you are done with an object and want to free memory or any other resources the instance allocated. Because Java includes a garbage collection feature for the automatic release of unreferenced memory, the role of destructors is much smaller than it is in Delphi. In Delphi, a destructor is necessary so that it can free any memory allocated by the object. Because of Java's automatic memory management, destructors are no longer needed to perform this job.
For these reasons, Java classes do not include Delphi-style destructors. Instead, each Java class can include a finalize method that can be used to perform any object cleanup. The finalize method is declared in the Object class, but because Object is the ultimate base class of all Java classes, finalize is available to every Java class. There is one danger, however, to consider when using finalize. It is possible for a Java program to terminate without this method being invoked on every object. If a program terminates with objects that are still referenced, the garbage collection process will never be used to release those objects, and finalize will never be called.
Inheritance in Java is indicated by the use of the extends keyword, as shown in the following example:
public class Employee extends Person {
// member methods and variables
}
This is the equivalent of the Delphi statement Employee = class(Person). If a class is derived directly from Object, then the extends keyword is optional. The following two class declarations are equivalent:
public class Person extends Object {
// member methods and variables
}
public class Person {
// member methods and variables
}
Where Delphi uses self to allow an object to reference itself, Java uses this for the same purpose. Similarly, Delphi uses inherited to reference the immediate superclass of an object, but Java uses super. The use of super is frequently seen in the constructor of a subclass, as shown in the following:
public class Person {
String firstName;
String lastName;
Person() {}
Person(String fName, String lName) {
firstName = fName;
lastName = lName;
}
}
public class Employee extends Person {
float salary;
Employee(float sal, String fName, String lName) {
super(fName, lName);
salary = sal;
}
}
In this example, the Person class includes a constructor that is passed a first name and a last name. The Employee class is derived from Person and includes a constructor that is passed salary, first name, and last name. The constructor for Employee first uses super to invoke the constructor for Person and then sets the internal salary member.
Exactly like Delphi, Java supports the declaration of abstract methods. Additionally, Java takes this a step further and introduces the concept of an abstract class. Instances of Java abstract classes cannot be created with new. Abstract members and classes are identified by the use of the abstract keyword, as shown in the following:
abstract class Species {
...
abstract void GiveBirth();
...
}
A class is considered abstract if it has one or more methods that are abstract. In the case of the Species class, the method GiveBirth is specified as abstract because some species have live births and others lay eggs. Because the method is abstract, no method body is given.
If you design a class that is entirely abstract, then that class is what Java refers to as an interface. A Java interface is similar to a class in that it defines a new type that contains both methods and variables. However, because an interface is completely abstract, its methods are not implemented within the interface. Instead, classes that are derived from an interface implement the methods of the interface.
An interface is declared in the same manner as a class except that instead of class, the keyword interface is used. For example, the following code will declare an interface named Clock:
interface Clock {
public String GetTime(int hour);
}
To derive a class from an interface, use the keyword implements, similar to the way extends is used when a class is derived from another class. To derive the classes Cuckoo and Watch from the Clock interface, you would do the following:
class Cuckoo implements Clock {
public String GetTime(int hour) {
StringBuffer str = new StringBuffer();
int i = 0;
while(i < hour) {
str.append("Cuckoo ");
i++;
}
return str.toString();
}
}
class Watch implements Clock {
public String GetTime(int hour) {
return new String("It is " + hour + ":00");
}
}
Java, like Delphi, does not support multiple class inheritance. In other words, a class may have only one immediate superclass because only a single class name can follow extends in a Java class declaration. Fortunately, class inheritance and interface inheritance can be combined when deriving a new Java class, and a subclass can implement more than one interface. For example, you can do the following:
class MySubClass extends MySuperClass implements FirstInterface,
SecondInterface {
// class implementation
}
There are a number of additional differences between Java and Delphi beyond those already mentioned in this chapter. This section will briefly describe some additional differences.
In Delphi, the program's command-line arguments are accessed through the functions ParamCount and ParamStr. These functions can be used to retrieve the number of command-line arguments and the text of each argument, respectively. The value returned by ParamCount will always be at least 1 because the first argument on the command line to a Delphi program is the name of the program. In a Java application, the command-line arguments are based in an array of String objects. The signature for main is as follows:
public static void main(String args[]);
Each component of the array args
is one of the command-line arguments. A difference
between Delphi and Java is that the program name is not passed
to the program as the first command-line argument in Java. Consider
two programs that are invoked in the following
manner:
For Delphi: program 100 200
For Java: java program 100 200
The command lines of these two programs will be interpreted by
Delphi and Java, as shown in Table 5.6.
Argument | Delphi | Java |
program | ParamStr(0) | (none) |
100 | ParamStr(1) | args[0] |
200 | ParamStr(2) | args[1] |
Delphi allows you to use a shorthand notation and invoke a procedure or function that received no parameters by leaving off the parentheses. For example, in Delphi you can invoke a parameterless procedure as myFunc. In Java, however, the parentheses are required and you need to use myFunc().
Java shows its C++ influence with its for statement. Fortunately, the C++ and now Java for statements are very powerful and easy to use. The first line of a for loop allows you to specify a starting value for a loop counter, specify the test condition that will exit the loop, and indicate how the loop counter should be incremented after each pass through the loop. The syntax of a Java for statement is as follows:
for (initialization; testExpression; incremement)
statement
For example, a sample for loop may appear as follows:
int count;
for (count=0; count<100; count++)
System.out.println("Count = " + count);
In this example, the initialization statement of the for loop sets count to 0. The test expression, count < 100 indicates that the loop should continue as long as count is less than 100. Finally, the increment statement increments the value of count by one. As long as the test expression is true, the statement enclosed in the loop will be executed.
You probably won't shed any tears, but the goto statement is not part of Java. On the other hand, it is still part of the reserved word list, so it may come back at any time. Java does replace goto, however, with the ability to use break and continue with labels. You can still use break and continue as you are used to in Delphi, but you can now use them to pass control flow in other ways. For example, consider the following code:
int line=1;
outsideLoop:
for(int out=0; out<3; out++) {
for(int inner=0;inner < 5; inner++) {
if (foo(inner) < 10))
break outsideLoop;
else if (foo(inner) > 100)
continue outsideLoop;
}
}
In this example, if the foo method returns a value less than 10, the code break outsideLoop will execute. A normal break here would break out of the inner loop. However, because this is a named break statement, it will break out of the named outer loop. This example also demonstrates the use of continue with a label.
This chapter covered quite a lot of territory. Building on your background in Delphi, you learned how Java differs from Delphi in regards to data types, operators, memory management, and classes. You've been introduced to some new operators that don't exist in Delphi, and you've learned how to use Java to create new classes. You learned that Java, like Delphi, supports only single class inheritance, but that Java's introduction of interfaces takes Java well beyond Delphi in supporting multiple interface inheritance. Finally, you learned that by removing pointers and incorporating automatic memory management in the form of garbage collection, Java programming is greatly simplified.