|
My background as a developer is C++ in the dim past, followed thankfully by Delphi for the last 5 ½ years or so. However, when I started working for Borland a few years ago, it became clear that I was going to have to learn Java. I tried hard for awhile to avoid it, as it seemed on the surface to be too much like C++, however eventually it became clear that I had to bite the bullet.
So I began to explore the language, with the help of a couple of books and some patient Java developers working with me at Borland, and eventually I came to regret my earlier C++ comparisons and appreciate the language. However, I still had a lot of trouble convincing other Delphi developers that Java was worth taking the time to learn. This is in spite of the fact that I believe Delphi developers are in a better position than a lot of other developers to learn Java. I think Bruce Eckel, that marvelous author of “Thinking in Java” and “Thinking in C++”, summed it up nicely in the introduction to his Java book:
“It's not that much of a surprise to me that understanding Delphi helped me understand Java, since there are many concepts and language design decisions in common. My Delphi friends provided assistance by helping me gain insight into that marvelous programming environment.”
Finally, Glenn Lawrence, President of the Australian Delphi User Group gave me the final push by suggesting I prepare a tutorial on the subject for the Australia BorCon in 1999, and the result is what your reading at the moment.
Why Should a Delphi Developer Learn Java?
You're reading this paper, so I assume you've got your own reasons for wanting to learn Java. Some people learn it because their boss has told them to, some because they need cross-platform ability. Others learn Java from a purely practical point of view, they want to ensure they have marketable skills, and currently Java is a very marketable skill to have.
But even if you never intend to use Java on a real project, I see benefit in becoming familiar with it as a Delphi developer. I believe that becoming familiar with Java, the classes supplied in the standard library, and the techniques used to solve certain problems will help you to write better Delphi code. I'll raise practical examples during this paper, but I think your Delphi coding can only benefit from becoming familiar with Java.
Goals
As much as I'd love to, I'm never going to be able to teach you to be as productive in Java as you are in Delphi in a single tutorial. What I hope to do is to get you over that initial hump, that first strangeness that you feel when you first look at a new language. If you can walk away from this paper, confident enough to begin exploring Java, then my goal has been reached.
To do this, I'll introduce you to the language and it's constructs. We'll look at creating and declaring objects, inheritance, polymorphism, and interfaces, JavaBeans, visual design, and exception handling. All of this with a slant on example code both in Delphi and in Java, so you can compare the differences.
I'm assuming, however, that you're familiar with all the concepts that I'm going to talk about in Delphi. For example, when I talk about interfaces, I'll show you how to use them in Java, but I'm not going to teach you what they are. I'm making the assumption that you already know what interfaces are, and how to use them in Delphi. Of course, where I'm covering a topic which isn't in Delphi, I'll spend some time teaching you the concepts.
So, let's get started.
Java Environment
Java is really made up of two pieces: a runtime environment and a development environment.
Development Environment
The development environment consists of
- Your source files, which have a .java extension. There is typically one file per public class, where the filename is the same as the class name. Eg. Customer.java would contain the Customer class. We'll come back to this later, once we've covered scope.
- The Java compiler (javac.exe on Windows)
- And a collection of pre-written classes, which make up the standard libraries.
You compile your source files with the java compiler, and for each java file you'll end up with a .class file with the same name. So for example, our Customer.java file, when compiled, will result with a Customer.class file. Our final application is really just a collection of classes working together, so what you end up with is a collection of .class files.
Runtime Environment
The runtime environment consists of:
- All your .class files produced by the java compiler
- The Java interpreter (java.exe on Windows). It's this piece which you'll see referred to as the VM or Virtual Machine.
The VM executes your class files as required, reading the compiled java byte code and translating this into instructions specific for the hardware platform and Operating System. It's the VM which contains all the platform specific code, so your application doesn't have to.
The Dreaded CLASSPATH
So if your application is really just a collection of class files, how does the VM find all these files at runtime? It does this using a concept called the CLASSPATH. For the moment, think of the CLASSPATH as being similar to the DOS PATH. Just like Windows uses your PATH at runtime to find required DLL's, the VM will look through all the directories defined in your CLASSPATH, looking for a .class file with the same name as the class it requires. Again, just like Windows, the first match it finds will be used, and it will stop looking through the rest of the directories. Again, like PATH, in Windows the CLASSPATH is usually set up as an environment variable.
Probably time for an example. Given the following environment variable:
CLASSPATH=c:javaclasses;d:myclasses;c:vbrokerlib
let's trace through what happens when the VM needs a class called Stack. First it will look for a file called c:javaclassesStack.class. If it is found, then the search will halt and the VM will use the class it's found. However, if it isn't found, the VM will move on to the next directory, in this case d:myclassesStack.class. Then it will try c:vbrokerlibStack.class. If after it has exhausted all items in the CLASSPATH and it still has not found a match, it will respond with a ClassNotFound exception.
Get used to this exception, as you'll see it a lot when you start experimenting with Java. I would say that 90% of all the problems I encountered when beginning to program Java were related to CLASSPATH errors. CLASSPATH's are actually more involved than I've outlined, although not much more. We'll cover CLASSPATH's again once we've talked about packages and JAR files, however the basic concept is the same. Don't worry, once you've used them a little they'll become second nature.
JDK, JRE, Java 2, J2SE,….???
If you've done any reading about Java, you've no doubt encountered JDKs, JREs and an assortment of other TLAs (Three Letter Acronyms). This is IT after all, and no technology is ever going to be adopted without lot's of acronyms to keep ordinary people from understanding it. So what does it all mean?
Well, JRE stands for the Java Runtime Environment. You guessed it, the runtime environment we spoke about earlier. JDK stands for Java Development Kit, which actually includes the JRE and the development environment we spoke about. The JDK is what you'll use to develop and test your applications, but your users will only need the JRE.
Java 2 was originally just a cunning bit of marketing on Sun's part. In an attempt to make us all think that they'd completely reinvented Java, they renamed JDK 1.2 to Java 2. Then when they realized that nobody was fooled, they executed a move even more cunning than the last one (which was pretty cunning itself so that's really saying something), a move so brilliant in it's cunningness that it left Microsoft's marketing department speechless with envy of it's ability to consume so much press bandwidth and yet actually mean so little. Java 2 was no longer just JDK1.2, it was in fact a whole family of Java technologies: J2SE, J2ME and J2EE.
All sarcasm aside, what are we left with? J2SE stands for Java 2 Standard Edition and is basically JDK 1.2. J2EE stands for Java 2 Enterpise Edition, which is JDK 1.2 plus some classes which implement things such as Enterprise Java Beans (EJB), Java Server Pages (JPS) Java Messaging Service (JMS) and other technologies to run scalable, fault tolerant distributed applications. At the other end of the spectrum you have J2ME, which stands for Java 2 Micro Edition. J2ME is still being worked out, but at the moment it is definitely not based on JDK 1.2.
While the rest of this paper is just going to deal with J2SE, the beauty of Java is that whether you're going to be using J2SE, J2EE or J2ME, it's all just Java and the differences are much smaller than the similarities.
Lastly, before we move on, I should run through how JBuilder has followed this evolution. JBuilder 1 was released using JDK 1.1. JBuilder 2 was released with JDK 1.1.6, but included a feature called JDK Switching to allow you to develop and test your applications using different versions of the JDK. That was until Sun released JDK 1.2, changed some package names and broker JBuilder 2's JDK Switching, so JBuilder 3 was released using JDK 1.2, and with new, improved JDK Switching. Each of these version of JBuilder were written in increasing amounts of Java, but each still retained portions of the IDE written in Delphi and C++.
Finally in 1999, a version of the JBuilder IDE known internally as Primetime, was released. This version was written entirely in Java. So now, not only could the applications you write run on non-Windows platforms, the actual JBuilder IDE could also run on non-Windows platforms. Just to confuse matters further, Primetime was first certified on the Solaris platform, and was named JBuilder 3 – Solaris Edition. A cut-down version, akin to previous Standard versions, was then made freely available called JBuilder Foundation. Foundation was certified on Solaris, Linux and Windows. At the time of writing, Enterprise version of Primetime were still to be certified on Linux and Windows, and it was anticipated that they will be released as JBuilder 3.5. The rest of this paper will be using JBuilder 3 – Solaris Edition, running on Windows NT (well, it is Java, so it should run most places. It just hasn't been certified on Windows yet), however the concepts and tools I'll use will be similar in previous versions of JBuilder.
Coding
I've now managed to waffle on for 4 pages, and I've yet to show you any Java code. So, before I lose any more of you, I'd better get started.
Declaring Variables
One of the first things I should show you is how to declare a variable in Java. It's basically the reverse of Delphi. Whereas in Delphi you'd do something like:
myInt : Integer;
myPoint : TSimplePoint;
In Java, the same thing would look like:
int myInt;
SimplePoint myPoint;
In case you haven't picked it up yet, the type goes first, followed by the variable name. Some other differences include that there is no concept of a var section as we have in Delphi. You can pretty much declare your variable anywhere in your code. Also, you can declare and initialize at the same time, so something like the following is completely legal:
int myInt = 15;
Lastly, there is no concept of global variables. All variables in Java must be contained in a class or a method contained in that class. In fact, this doesn't only apply to variables, everything must be part of a class, including functions. This is the source of the white lie you'll hear touted by Java fans that “Everything in Java is an object”. It's plainly not, as you'll see in the next section, but nearly everything is an object, and more importantly, everything must belong to an object.
Primitives
So if not everything is an object, what else do your have? Well, primitives actually. Java has a small collection of primitive types:
- boolean
- char
- byte
- short
- int
- long
- float
- double
However, if you had your heart set on not using anything which isn't an object, Java helps you sleep a little better at night by providing object wrappers for each of these primitives:
- boolean - Boolean
- char - Char
- byte - Byte
- short - Short
- int - Integer
- long - Long
- float - Float
- double - Double
Hopefully you've noticed something in the list above which may or may not have made your skin crawl. That's right, Java is case sensitive and a float is a completely different beast than a Float. Once you're used to it, this shouldn't cause too much trouble.
So, why would we use these wrappers? Well, for one, they provide a whole heap of methods for converting and manipulating the primitive data type the contain. For example, have a look at the next piece of code:
char myChar = ‘x'; // primitive type
Character myCharacter = new Character(myChar); // Wrapper class
// Can then call methods on the wrapper class
myCharacter.toString();
myCharacter.compareTo(‘y');
We'll cover Objects shortly, but as you can see, once constructed, the wrapper class lets us manipulate simple data types in an OO fashion.
Basic Constructs
Ok, so let's have a look at some more of the language. We've covered the case sensitivity, so let's get some other simple differences out of the way.
Firstly, matching curly braces ie. { … } are basically the equivalent of the begin … end pair you know and love from Delphi. You'll see plenty of examples shortly, but for the most part, the same rules of when to use them apply.
Secondly, if you're calling a method with no parameters, you still need to put the open and close braces ie. () at the end of the method name. If you're anything like me, this will cause you no end of trouble for the first day or two, but you'll get used to it pretty quickly.
Operators
Mathematical Operators in Java are similar to those in C
- = is assignment, equivalent to := in Delphi
- + (addition)
- (subtraction)
- / (division)
- (multiplication)
- % (modulus)
These all have their equivalents in Delphi. However, the increment and decrement operators take a little getting used to. ++ increments a value by 1, and -- decrements a value. So far so good, but have a look at the following piece of code:
int x = 5;
x++; // x is equal to 6
x--; // x is equal to 5
++x; // x is equal to 6
--x; // x is equal to 5
System.out.println(x++); // 5 is printed, x is now equal to 6
System.out.println(++x); // 7 is printed, x is now equal to 7
As the first few lines demonstrate, you can place these operators either before or after the variable, but as the last two lines show, there is a difference. Placing ++ after a variable that is a parameter to a function means that the value will be passed in before the increment occurs. Placing it before the variable means the increment will occur and then the value will be passed in. This simple distinction can be the source of some hard to find bugs if you're not careful.
Relational Operators
With a few exceptions, relational operators are similar in both languages:
- == is a test of equality, equivalent to = in Delphi
- != is a test of inequality, equivalent to <> in Delphi
- < is less than
- is greater than
- <= is less than or equal to
- >= is greater than or equal to
It should be noted that as in Delphi, these will test the equality of the object instance (ie. do the variables point to the same memory address), not the object value. Use the Equals method, declared in the base Object class to test object equivalence
Logical Operators
We should also compare logical operators in the two languages:
- && is logical AND (equivalent to AND in Delphi)
- || is logical OR (equivalent to OR in Delphi)
- ! is logical NOT (you guessed it, equivalent to NOT in Delphi)
The if-else operator
Just before we get into execution control, there's an operator which doesn't have an equivalent in Delphi. That is, the if-else operator. Don't confuse with an if-else statement, this is an operator. The if-else operator looks like this:
boolean-exp ? value0 : value1
Where boolean-exp is an expression which evaluates to either true or false, value0 and value are any valid language statements, separated by a colon. What happens is that boolean-exp is evaluated and if true, the statements in value0 are executed. If false, the statements in value1 are executed. For example, this statement:
int x, y;
x = y > 10 ? y * 10 : y*100;
is exactly equivalent to this one:
int x, y;
if (y > 10)
x = y * 10;
else
x = y * 100;
So while you can save yourself some typing using the first one, your making life much harder for the person trying to read your code. If you're after my opinion, use Code Templates to save the typing and use the second form.
Execution Control
So, just before we move onto the interesting stuff, let's have a quick look at some execution control constructs. These really only differ to their equivalents in Delphi by their syntax, so I'll move pretty quickly:
if..else
|
This in Delphi: |
is equivalent to this in Java: |
|
var
x : Integer;
if x = 5 then
x := 100
else if x < 5
x := 50
else
x := 150; |
int x;
if (x == 5)
x = 100;
else if (x < 5)
x = 50;
else
x = 150; |
Note that in Java, a line preceeding an else statement still ends in a semi-colon. Also, that in both languages there is no need for begin..end/{..} pairs after each if-case as there is only one statement in each. If multiple statements were required, they would need to be enclosed in begin..end/{..}
while
|
This in Delphi: |
is equivalent to this in Java: |
|
var
x : Integer;
while x<10 do
begin
Inc(x);
end; |
int x;
while (x < 10)
{
x++;
} |
Again, note that in this case the begin..end pair in Delphi and the {..} pair in Java are not required, but can be included to aid readability.
do..while
|
This in Delphi: |
is equivalent to this in Java: |
|
var
x : Integer;
repeat
Inc(x);
until x >= 10; |
int x;
do
x++;
while (x < 10); |
Note that while the above code achieves the same purpose, the actual statement is different. The Delphi code says repeat this code until this expression evaluates to true. The Java code says repeat this code while this expression evaluates to true. All it means is that the boolean logic needs to be reversed.
switch
|
This in Delphi: |
is equivalent to this in Java: |
|
var
x : Integer;
case x of
0 : x := 5;
5 : x := 10;
20 : x := 15;
else
x := 0;
end; |
int x;
switch (x) {
case 0 : x = 5;
break;
case 5 : x = 10;
break;
case 20 : x = 15;
break;
default : x = 0
} |
The important thing to note in this example is the use of the break statement in the Java code. Whereas the Delphi case statement will find the matching entry and execute only that entry, the Java switch statement will find the matching entry, then execute that entry and each subsequent entry until a break statement is reached. If we removed the second break above and x was equal to 5, the code for case 5 would execute, and then execution would fall through to case 20. Only once case it hit the next break statement would it exit.
Objects
Remember that almost-truth I mentioned before about everything in Java being and object? Well, I guess it stands to reason that before we can start writing programs, we need to have a look at how Java implements the OO techniques you've come to know in Delphi.
But before we do, I just want to reiterate something I mentioned before. No code can exist outside of a class. Whereas Delphi will happily let you write a mixture of procedural and OO code, Java is very strict in this regard. You've probably built up over time units in Delphi which contain dozens of general purpose routines, they're not part of an object, they are standalone procedures and functions. In Java, this just isn't going to happen. At a minimum, you'll have to organize these functions into classes and make them all class methods. But as you'll see shortly, Java's implementation of objects comes pretty close to Delphi's in terms of clarity and elegance, so this shouldn't be that hard.
Class Definition
We've already talked about the fact that in general, your class will live in a .java file of the same name. We'll cover the exception to this rule when we get to scope, but in the meantime, the first thing we should look at is how to declare an object in Java.
|
This in Delphi: |
is equivalent to this in Java: |
|
type
TSimplePoint = class
public
x : Integer;
y : Integer;
constructor Create(x, y : Integer); overload;
end;
implementation
constructor TSimplePoint.Create(x, y: Integer);
begin
inherited Create;
self.x := x;
self.y := y;
end; |
public class SimplePoint {
public int x = 0;
public int y = 0;
//constructor
public SimplePoint(int x, int y) {
this.x = x;
this.y = y;
}
} |
A few things you should note about the Java class declaration:
- There is no separation of declaration and implementation. You implement the procedure at the same time as you declare it.
- The constructor has the same name as the class
- There is no call to the inherited Constructor (this is done implicitly for you)
- Scope identifiers (eg: public) are specified per item, not in groups Also, classes have scope identifiers as well as their contents.
- Self in Delphi is equivalent to this in Java.
Apart from these mostly syntactic differences, there's nothing really revolutionarily different.
Visibility and Scope
I've mentioned this topic a few times already, and we skirted around the edges of it in the last section, so we'd better get it out of the way. Java's visibility keywords are remarkably similar to Delphi.
Delphi -- Java
- Published -- no equivalent
- Public -- public
- Protected -- protected
- Private -- private
- no equivalent -- none (package)
As we saw in the earlier section, these keywords are specified per class member (both methods and variables) and their meanings for the most part are the same as those in Delphi. For instance, a protected item in Java is visible only to itself and to it's descendants. Published in Delphi has no real equivalent in Java. Java does have the concept of RTTI, but it isn't produced by specifying an item as published.
This is also a good time to come good on something I've been hinting at for the whole paper. I've said a few times that in general, you have one class per file. Well, this isn't strictly true. You only ever have one public class per file, and it's this class which gives it's name to the file. However, you can have as many other classes in the file as you like, as long as they are not visible to the outside world (ie. private, protected or package)
The last one is interesting. It has no real equivalent in Delphi, and it doesn't even have a keyword. If you do not specify a visibility for something, it defaults to package level visibility. This means that the item is visible to anything in the same package. What's a package? Well, that's the next section. (Gee, was that smooth or what?).
Packages
Think about this problem. I declare a public class called Stack, in a file called Stack.java, and compile it. What I end up with is a file called Stack.class. Another developer on my team also needs a stack, but his is different to mine. So he declares a class called Stack, implements it slightly differently, and compiles it into a file called Stack.class. We now have a problem. If you remember when we spoke about CLASSPATHs, the VM stops searching for a class when it finds the first one that matches. One of us is going to have problems when we declare an instance of a Stack, thinking it's our version, but in actual fact the VM instantiates the others Stack class.
The solution to this is packages. Packages allow you to split up the namespace, rather like directories in DOS. Now, my class isn't called Stack, it's called malcolm.Stack. My teammates Staack isn't called Stack anymore, it's called robert.Stack. Now we have no problems, as I now instantiate an instance of malcolm.Stack, and I know there's only one of them, right?
Well, not exactly. All I've done is reduced the likelihood of clashes, not removed them altogether. What we really need is some way to assign unique prefixes to anybody who wants them, so we can be sure not to have clashes. Well, rather than inventing a new naming body, Sun wisely decided to piggyback on an already successful one: the internet domain name registry. So, the convention is to prefix all you classes with your domain name reversed, and then make up package names under this. So, my stack class might actually be called com.borland.mgroves.utils.Stack, and if everyone plays by the rules, the only clashes I'm going to have are those I create myself.
But how does this change the CLASSPATH problem? Well, remember I said CLASSPATHs we're actually a little more complex than I let on? Now we can reveal some of that extra complexity. As we said before, if I try to instantiate Stack.class, the VM will walk through my CLASSPATH, attaching the class name to the end of each entry, trying to find the file. It's no different now, but what's changed is my class name, it's now com.borland.mgroves.utils.Stack. So with a CLASSPATH entry of c:javaclasses, instead of looking for c:javaclassesStack.class, the VM will now look for c:javaclassescomborlandmgrovesutilsStack.class. Each part of my package name becomes a subdirectory. The tricky bit is making sure that the CLASSPATH entry points to the root directory of your first package directory, ie. the CLASSPATH entry for the clas above should be c:javaclasses, not c:javaclassescom or any other combination. Again, once you've sorted out a few ClASSPATH problems, it will become second nature.
Lastly, we'd better look at how you specify the package. The package specifier should be the first, non-comment line in your .java file. So the top of my Stack.java file should look like this:
package com.borland.mgroves.utils;
public class Stack {
Creating an Instance
Knowing how to declare a class is no use if you can't instantiate one, so here's how to do it:
|
This in Delphi: |
is equivalent to this in Java: |
|
myPoint := TSimplePoint.Create; |
myPoint = new SimplePoint() ; |
The new keyword allocates space, calls the constructor and returns the instance. If you remember before I said that in Java you can declare and instantiate a variable all on one line, so the following is perfectly legal:
SimplePoint myPoint = new SimplePoint();
Destroying an Instance
It would seem to follow that I should now tell you how to destroy an object. OK, you don't. Let me say that a second time, you don't destroy objects. Just let them go. Overwrite your reference to them with another object, let them go out scope, who cares. Just let them go.
Now, this may seem a little hard to swallow initially, especially if like me, you've spent hours in the past tracking down memory leaks in your Delphi code caused by creating an object and not destroying it. But once you get used to it, it's almost liberating. The temptation to run around dropping objects left, right and center is almost to much to resist.
Well, this is the real world and someone usually comes along to spoil your fun, and unfortunately, that's my job. Don't get carried away with this “Just let it go” thing. There's a danger in it, and it's not a bug, it's how it's meant to work. Let me explain.
When you drop a reference to an object, it isn't destroyed straight away. The VM will, whenever it decides it needs to, start a garbage collection process to free up all those objects you've let go. When it goes to free an object, it calls the finalize() method, giving you a chance to do whatever you want to before the object goes away. Maybe in your constructor you opened a tcp/ip socket, so in your finalize method you close it.
All will be OK, provided the garbage collection runs. The problem creeps in because garbage collection is not guaranteed to occur. What happens if you close your application before the VM has decided it needs to collect garbage? Well, the memory will be released in one fell swoop, and your socket will never be closed. have this happen enough times and your socket server will start to look decidedly ill.
So, the solution is to include a method in your object, called CleanUp() or whatever you like, where you free any resources you might have created. Call this manually when you know that you're done with you object, just like a destructor in Delphi. I know this removes all the fun of “Just let it go”, but it's better than leaving resources orphaned all over the place.
The Main Routine
We're nearly at the point where we can write our first, working Java program. We just have one thing to cover. The very observant among you may have already nnoticed a bit of a problem. Think about how a program in Delphi is started. In your .dpr file, you have some procedural code which runs, creating your main object and then you're away. Well, I've already mentioned that you can't write procedural code in Java, all code must be contained in a class. So how do we kick the whole process off in the first place?
This bootstrap problem is solved by a static method called main. A static method is basically the same thing as a class method in Delphi, a method you can call with only a reference to a class, not an object. Now that we have this, we can define a static emthod with a particular signature, and the VM will look for this method and call it for us. Once that happens, we're under way and we can start creating and using other objects in our code. Let's have a look at an example:
public class Hello
{
public static void main(String[] args)
{
Hello hello1 = new Hello();
System.out.println("Hello World");
}
}
This is nearly the smallest program you can write in Java. Let's walk through what it does:
- It's a public class, with a public, static method called main, which is a procedure (it returns nothing, as signified by the return type of void just before the method name.) which takes an array of Strings as a parameter. This array is essentially the equivalent of ParamStr in Delphi, the command line parameters specified when starting the program.
- Inside the main method, we create an instance of the class. In this program we don't actually do anything with it, but we could call methods on it if we desired.
- We then print out to the console the string ”Hello World”
- The main method then ends, causing our program to end.
Hello World
So let's use the last code example shown, and actually run it. We'll use JBuilder shortly, but for now, let's use the command line. Create a file called Hello.java, and type the code from the previous example into it. Then at a command line, in the directory where you placed the .java file, type:
javac.exe Hello.java
This will cause the Java compiler to compile your source code into a file called Hello.class, and place it in the same directory as your .java file. What you have to do know, is copy it to a directory which is on your CLASSPATH, then type the following command to run the program:
java.exe Hello
Provided you've got everything set up correctly , you should see those magic words, “Hello World” on the screen.
Inheritance
We're not done with objects yet. What about inheritance? Well, like Delphi, Java only supports single implementation inheritance, and the declaration of your classes ancestor is similar also:
|
This in Delphi: |
is equivalent to this in Java: |
|
// class descending from TObject
TSimplePoint = class
public
// class with explicit ancestor
TMalPoint = class(TSimplePoint)
public |
// class descending from Object
public class SimplePoint {
// class with explicit ancestor
public class MalPoint extends SimplePoint { |
Overriding methods in Java is much simpler than Delphi. Just implement the method in the child class with the same signature as the parent, and you're done. If you want to call the parent method (ie. Delphi's inherited ) use super, eg :
super.mymethod()
Most methods can be overridden, provided they are declared as public or protected, and they aren't final. If you declare a method as final, it prevents it from beign overridden in a descendant class. eg.
public final int MyMethod();
Polymorphism
Polymorphism in Java works much as in Delphi. Given the following two classes:
|
Delphi: |
Java: |
|
TAnimal = class
TDog = class(TAnimal) |
public class Animal {
public class Dog extends Animal { |
you can:
- substitute a Dog where an Animal is expected (upcast)
- call methods on a Dog through an Animal handle
- cast an Animal handle to a Dog (downcast). This will be type checked automatically by the VM.
((Dog)myAnimal).dogmethod()
Interfaces
Java, just like Delphi, supports single inheritance for your implementations. However, also like Delphi, it does support multiple inheritance for Interfaces. The biggest difference is not a technical one: I've met plenty of experienced Delphi developers who have never used interfaces, but I doubt you'll meet too many Java developers who haven't. While Delphi supports Interfaces, the bulk of the VCL was developed before this support was added, so many Delphi developers remain ignorant of their use. The standard Java classes, however, use Interfaces all over the place, so it's pretty hard to not use them. In addition, Interfaces allow incredible flexibility in your object designs, so regardless of which language you'll be implementing in, I recommend getting familiar with them.
I'm not going to teach you about Interfaces in Delphi in this paper. If you want to find out about them, a good starting point is section 3.4 of the Delphi Developers Guide, which comes with Delphi. However, if you are already familiar with Interfaces in Delphi, here's how they work in Java.
|
Java: |
|
interface instrument {
public String getName();
public String play();
}
|
The above code defines an interface called instrument, which has two methods, getName and play, both of which return Strings. To implement this interface, have a look at the following code:
|
Java: |
|
class Guitar implements instrument {
public String getName(){
return "Guitar";
}
public String play(){
return "Twang";
}
}
class Drums implements instrument {
public String getName(){
return "Drums";
}
public String play(){
return "Bang Bang Bang";
}
} |
This code provides two implementations of instrument. Let's have a look at the Guitar class. It descends from Object (this is just a normal java class, you can descend from whatever you like) but on the first line there's some new additions, namely, the implements keyword and the name of our previously defined interface. Then, in addition to any other methods and attributes the class may have, it must provide implementations of the methods defined in the instrument Interface, in this case, the getName() and play() methods. The Drums class is similar, so I won't go through it.
Now, how do we use these classes via their instrument Interface? Have a look at the following code:
|
Java: |
|
instrument inst = new Guitar();
System.out.println(inst.play()); |
This is a very simple use of interfaces, but let's walk through it before we look at a slightly more complex example. Instead of declaring as variable of type Guitar, we declare it as the type of our Interface, instrument in this case. Then we can create an instance of any class which implements our interface and store it's handle in our instrument reference. Now we can use our instrument reference to invoke any of the methods declared by that interface, and the implementation of those methods in whichever class we created will be invoked.
In case you're not experienced with interfaces, what we've basically got is the same polymorphism we looked at earlier, but instead of relying on our classes all descending from some common base, we only rely on our classes implementing the same interface. We can implement an interface on wildly different classes, which share no ancestors (except Object, which all Java classes share) and treat them polymorphically. Wow, this is flexibility!
Let's look at a slightly more involved example, this time involving a Vector and an Iterator, along with our instrument Interface and our Guitar and Drums classes. If you're like most Delphi developers, you use the Tlist class as a bit of a general workhorse class. Well, you'll be pleased to know Java has an equivalent class called java.utils.Vector. Like a Tlist, a Vector let's you add objects to a list, and reference them by their index. Also, like Tlist, they're stored as Object instances, and you'll usually need to cast them back to some Object descendant to use them.
Vectors go one step further however, by providing you with an Iterator. An Iterator is an interface which allows you to navigate through a collection of objects, safely and ignorantly. Let me explain what I mean by safely and ignorantly.
Let's say you want to walk through the list, invoking some method on each object you've stored in it. You'll probably end up doing something like for I := 0 to List.Count – 1 and then casting List.Objects[I] to whatever class it is. This brings up 2 issues. Firstly, it's pretty easy for inexperienced developers to try for I := 1 to List.Count , not realizing that Tlist is zero-based. This raises the second issue, it relies on you knowing the implementation details of the Tlist. I thought this object stuff was meant to shield client code from knowing implementation details? Well, that's exactly what Iterators do. They let you visit each item once and only once, and to do it without knowing whether the collection of objects is a list, a tree, or whatever other data structure you can think up. Let's have a look at how you use them:
|
Java: |
|
Vector v = new Vector();
v.add(new Guitar());
v.add(new Drums());
v.add(new Guitar());
v.add(new Drums());
v.add(new Drums());
v.add(new Guitar());
v.add(new Guitar());
v.add(new Drums());
Iterator i = v.iterator();
instrument inst;
while (i.hasNext()) {
inst = (instrument)i.next();
System.out.println(inst.getName() + " : " + inst.play());
} | Creating a Vector and adding objects to it is pretty straightforward. Then we declare a new Iterator, and instead of creating one, we ask the Vector for it. An Iterator is an interface, and different implementations of it are available: one that knows how to navigate a Vector, another that knows how to navigate a tree, etc. But we treat it as the Iterator interface, thereby providing us with implementation ignorance we're after. Then by using the hasNext() method to ensure that we're not at the end of the list yet and the next() method which returns the next object, we can cast the object to our instrument interface and call it's methods. If you're like most people, once you start using Iterators and interfaces in general, it'll be very hard to go back.
Exception Handling
With a few exceptions (pardon the pun) that I'll cover shortly, exception handling in Java is very similar to exception handling in Delphi. As in Delphi, Java exceptions are classes which ultimately descend from Exception. To throw an instance of the base exception class, you use the throw keyword, which is similar in intent to the Delphi raise keyword:
|
Delphi: |
Java: |
|
raise Exception.Create('An exception occurred');
|
throw new Exception("An exception occurred");
|
So if you can throw an exception, how do you catch one? Like a Try..Except in Delphi?
|
Delphi: |
Java: |
|
try
//Code that may cause exception
except
on MyException do
// Handle exceptions of type
// MyException;
on Exception do
// Handle exceptions of type
// Exception;
end; |
try {
//code that may cause exception
}
catch (MyException ex){
// Handle exceptions of type
// MyException
}
catch (Exception ex) {
// Handle exceptions of type
// Exception
} |
Or maybe a Try..Finally?
|
Delphi: |
Java: |
|
try
// code that might cause an
// exception
finally
//code that will run whether an
// exception is thrown or not
end; |
try {
// code that might cause an
// exception
}
finally {
// code that will run whether an
// exception is thrown or not
}
|
Interestingly, you can combine both in one handler, something which is not so elegantly achieved in Delphi:
|
Java: |
|
try {
// code that might cause an exception
}
catch (Exception ex){
// Handle exceptions of type Exception
}
finally {
// code that will run whether an exception is thrown or not
} |
In Delphi, any exceptions which go unhandled will eventually be handled by the Application object, which will fire an OnException event to give you a chance to respond. The closest equivalent in Java is to wrap the contents of your main routine in a try..catch statement.
Interestingly, the Java compiler enforces a couple of obligations on you when it comes to Exceptions. Before it will compile your code, it forces you to either:
- Handle all potential exceptions, or
- Explicitly declare which exceptions you don't handle, thereby pushing the responsibility of handling them off to the calling code.
All exceptions must eventually be handled, and the Java compiler will enforce this by refusing to compile an application which allows exceptions to go unhandled. So how do you know which methods might be thrown from a particular method? The author of the method must declare in her code which exceptions could escape from her method. This is done using the throws keyword in your method declaration:
void MyMethod(int a) throws notValid, MalsException
You can declare that a method throws an exception which it actually does not. however you however you cannot omit an exception that is thrown.
Sometimes you want to re-throw an exception that you've already caught, allowing it to continue up to a higher level try..catch routine. Whereas in Delphi you'd use the raise keyword, in Java you use throw, eg:
|
Delphi: |
Java: |
|
try
// code that might cause an
// exception
except
on MalsException do
begin
// Do anything you want
raise ;
end;
end; |
try {
// code that might cause an
// exception
}
catch (MalsException ex){
// Do anything you want
throw ex ;
} |
Lastly, you can create custom exception types in Java by simply creating a descendant of some existing exception, just as you would in Delphi. Exceptions are classes, so you can add your own properties, methods, etc. eg:
|
Java: |
|
class MalsException extends Exception {};
class MalsMoreSpecificException extends MalsException |
JAR Files
We saw earlier that our CLASSPATH defines all the class files visible to our app. However, having 200 class files to deploy with your app, in the correct directory structure to match their packages, would be a real pain. The solution is something called a JAR file. JAR files solve this by acting like a file system inside a file.
JAR files are basically ZIP files. All the desired .class files are compiled into the JAR, with their relative paths
intact. The JAR file then get's added to the CLASSPATH. The VM is smart enough to treat the JAR file like a directory and look inside it for the desired .class file when it strikes one in the CLASSPATH. This adds the missing piece to our discussion of CLASSPATHs earlier. Our CLASSPATH might now look something like:
c:iasclasses;d:toolsvbrokervbjorb.jar;d:toolsjavaclasses
Components
Just as Delphi has the VCL, Java has JavaBeans. JavaBeans is the component spec for Java, and if you’ve written any VCL components, you’ll notice a lot of similarities in JavaBeans. This isn’t surprising as the JavaBeans spec is heavily based on concepts in the VCL, and Borland was one of the main companies involved in the development of the JavaBeans spec. JavaBeans development is beyond the scope of this paper, but using we should talk about a couple of the differences before talking about the standard Beans that come with Jbuilder.
In Delphi, a component is a component because it descends from a specific class: Tcomponent. In java a Bean isn’t a Bean because it descends from a specific class. A Bean is a Bean if it implements certain coding conventions. For example, your properties are implemented using get and set methods. Look at the following code:
Delphi: Java:
private
function getFullname: String;
procedure setFullname(const Value: String);
public
published
property Fullname : String read getFullname write setFullname;
public void setFullname(String newFullname) {
}
public String getFullname() {
}
Whereas we define a property in Delphi using the property keyword, and have the option of using methods or direct access to read and write the value, in Beans we define a property by having a get and/or a set method. In Delphi to set or get the above property value, you could reference the Fullname property, in Beans we need to explicitly call setFullname or getFullname. This is one of the things I miss when doing Java work, as I think the property comcept in Delphi is incredibly powerful.
So, check out the references section at the end if you want to start writing JavaBeans, but lets have a look at the standard groups of Beans you’ll find in Jbuilder.
AWT
The Abstract Windowing Toolkit (AWT) was Sun’s original set of GUI Javabeans for Java. They are implemented as heavyweight controls (ie. They rely on a control on the Host OS to do the actual painting, rather like descendants of TwinControl). As a result, on Windows your AWT Edit control will look like a Windows Edit control, on Unix it will probably look like a Motif Edit control.
While this sounds desirable, it usually turns out not to be. Firstly, it makes laying out your form complex in anything but simple apps, as you can’t rely on what the native controls will render as. Also, AWT ends up being a lowest-common-denominator approach, so for the most part, the controls are simple, and customizing them becomes difficult.
Swing
Recognizing the limitations of AWT, Sun introduced a new component library: Swing. Swing components are implemented as lightweight controls: they are completely responsible for painting themselves, implementing their behavior etc. Customisation of these controls is now possible, as the entire control is implemented as Java code. But if you liked the idea of your app looking like whatever OS platform you’re running on, Swing also implements customizable look-and-feel. It’s up to you whether you control looks like it’s native OS representation, the same across all OS’s, or even like Motif on Windows, Windows on Motif, or a look-and-feel unrelated to the OS platform.
Swing is very powerful, but it’s also quite complex. Have a look in the References section at the end of this paper for more resources.
JBCL
JBCL is Borland’s JavaBean library, including extensions to the standard Javabeans, and completely new Beans. JBCL adds things like data-aware controls to the standard libraries. Originally implemented on top of AWT, it’s now implemented on top of Swing.
Which should I use?
Like any class library, it will take some time to get familiar with the functionality any of the above libraries offer. An important question though, is which one should you use? As always, this is going to vary depending on your application requirements, but the paragraph below should give you a start.
If you're writing applets, use AWT. Yes it's primitive, yes it's plain looking, but most importantly, it's provided with most Java-compliant web browsers, so your applet users won't have to download all the AWT classes, just your applet classes. This can mean the difference between downloading a megabyte of class files and downloading a few kilobytes of class files. Swing on the other hand, is not included with most browsers, nor is JRE 1.2, so your users will be up for a lot of downloading.
If you're writing applications, and you can get a JRE 1.2 for your target platforms, then consider Swing, as it will let you create much richer UI's. If not, consider using AWT.
Whichever of the above you're using, if you use JBCL, you'll need to deploy the JBCL classes, which could increase the download/deployment size of your app markedly.
Conclusion
Java is big. (This comment is like people standing beside the Grand Canyon saying “Gee, it's big.”) What I've hoped to do is get you familiar enough with the language and the environment so that you can start exploring the class libraries (the biggest part). Hopefully I achieved that. Whether you ever do a project in Java or not, I hope the techniques you learn by exploring Java make you a better developer, regardless of the language.
References
Books
- Java Class Libraries, Michael Lee
- This is a fantastic reference to the standard Java class libraries
- Thinking in Java, Bruce Eckel
- Sun, The Java Tutorial
Online
- Java Developer Connection
- Jbuilder
- JavaWorld
- Newsgroups
- Swing
Download Source Code
|