Java is a programming language that provides a foundation for developing Internet applications. Java does this through applets, which are programs executed as part of a Web page and displayed with a Java-enabled browser, such as HotJava or Netscape Navigator 2.0. However, Java's features need to be understood in a context much broader than just running Internet applications. Java is a language designed to solve a variety of problems that have plagued the computing community for years; running well on the Internet is almost a by-product of solving these problems.
What are these problems? And how does Java solve them? This chapter will attempt to answer those questions. Through the course of this chapter, you will be given the background needed for understanding why Java, applets, and the Internet are such a natural match. This background will not only help you better understand the techniques used to program the Java applets developed throughout this book, but the extra knowledge of the "big picture" may also help you come up with novel ways to apply this new technology. Java and the Internet are very much like the old Western frontier-pathfinders who break new ground may find the gold that lies underneath.
A key word often used when discussing the "information superhighway" is convergence. People talk about the convergence of telecommunications and computing, television and computers, consumer appliances and telecommunications, and so forth. Each of these converging factors comes from areas of technology that, until recently, have not had much in common. For example, the technology of fiber optics does not have much to do directly with the problems of computing. At the same time that these disparate technologies have converged, there have also been pressures from within computing to bring together disciplines historically considered unrelated. This convergence within computing has been brought about by various pressures that have arisen:
Each of these problems has been seriously addressed from many fronts, but no approach has simultaneously addressed all these problems. For example, there might be a programming language that's good for rapid development but isn't portable, or a development environment might be portable, but could not work in a distributed environment without opening you up to serious breaches of security. A single solution to all these problems has been lacking until Java!
The designers of Java created a programming environment to attack each and every one of these problems. As a comprehensive solution to these pressing issues, Java needs to be understood as not just a programming language, but as a general-purpose environment. Its unique combination of a programming language, compiler, and runtime environment provides a general architecture well suited for addressing many of the concerns that have been plaguing the computing community for years. This general development environment is often referred to with the solitary term Java.
In part, Java's evolution into a comprehensive solution stems from its twisted history of being a language for general consumer electronics to being one for PDAs and set-top boxes for interactive television. Features common to each of these potential platforms include severe memory constraints and the need to support multiple operating systems. These problems forced the language to address the problems of distributed computing, efficiency, security, portability, and reliability. For Java to be a successful language, furthermore, it would also have to address the problems associated with the software crisis-it would need to be object-oriented.
Being object-oriented carried another bonus for Java. The object-oriented message-passing model of invoking class methods makes it a natural for network-based application development. Consequently, being object-oriented is one of the cornerstones of Java, so it's a good place to begin a tour of its general architectural features.
Why is it important for Java to be an object-oriented language? As stated before, modern programming languages need to address the problems of the software crisis. Perhaps the most serious difficulty a software engineer encounters is the inherent complexity involved in modeling a real-world problem. Through the past decades of computing, analysis techniques have come in and out of fashion that aim to make modeling the world a more comprehensible and stable practice. When you were taking your first classes in programming, you might have seen flowcharts used as a modeling tool. The Structured Analysis school of thought uses data flow diagrams to model the world. These approaches have some contributions to make, but they have one serious problem: they are heavily oriented toward the procedural side of the activities being modeled. The problem with a procedural approach is that it does not translate well to software that is compact, easy to maintain, and reusable. In its worst implementations, a system based on the procedural model has a different function for every different type of procedure that could occur. You have probably seen such systems-they are so incomprehensible and hard to follow that the only goal of the poor soul who has to maintain the system is to "just make it work."
Object-oriented analysis, design, and programming is a radical departure from the procedural model. Its focus is not on the procedures of the modeled world, but on the objects appearing in it. Objects are a natural thing to model because that's what the world is made of. Planes, dogs, and computers are all objects. Even abstract concepts, such as a priority queue, can be thought of as an object. What all objects have in common is that they have state and behavior. For example, the state of a plane can be partially indicated by its speed and location, but its behavior might be represented by its ability to change altitude.
Another feature of objects is that they are self-contained; in other words, they are modular. When modeling a car engine, for example, you can focus on the characteristics of each object in the engine as opposed to the flow of gas and energy through it. The object model breaks the engine down into a series of components that can be described on their own terms, as opposed to what they are in a complex global view of things. Instead of writing a single huge procedure describing what's going on in the car, you can focus on how each of the individual elements behaves.
Finally, objects have the quality of being hierarchical. A large complex object, a house, for example, consists of other objects, such as its physical structure, the electrical system, the plumbing, and so forth. These are in turn made up of yet other objects, such as a chimney, the light switches, and a sink. These can be broken down into yet smaller components such as wood, wires, and pipes. By modeling a system based on hierarchy, you can use the model of smaller, more easily understood objects, such as a wire, to construct more complex objects, such as a light switch. Since the switch is now seen as an object that consists of yet simpler objects, its complexity is easier to understand and manage.
In object-oriented systems, objects communicate through a message-passing mechanism. When an object receives a message, it may change its state and behavior as part of the response. For example, sending a message of "play" to a CD player object might result in the CD playing a music track. This message-passing aspect of objects translates very nicely into the needs of network programming, where transactions are carried out by the mechanism of messages. It is particularly well suited to distributed systems because objects communicate by a standard message-passing mechanism, in which the actual location of the objects becomes less important. If your object needs to talk to an object, it isn't particularly important if it is on your computer or a remote host. All that really counts is that the object gets your message.
Java is unusual for an object-oriented programming language in that it is "objects all the way down." Unlike C++, which is a confusing combination of objects and functions, everything in Java is an object. Strings are objects, numbers are objects, threads are objects, even applets are objects. Because of this, Java has all the helpful features of object-oriented systems just described. Its core constructs of classes, objects, methods, and instance variables are, by their very nature, managed in a modular fashion. Java's support for inheritance allows you to build new classes from other classes. Each class you construct becomes a tool that can be used to create yet more complex classes.
Java's particular object-oriented implementation allows it to have yet even more desirable features than those already discussed. It has a runtime garbage collector that removes objects from memory that you no longer need. No longer will your program run out of memory because you forgot to explicitly delete an object. The garbage collector, which your program doesn't even have to be aware of, does this for you. Furthermore, Java's total orientation toward objects removes another construct that has been a blight of programmers-the pointer. Java hides almost all aspects of memory management from the programmer. C or C++ programmers who have had to deal with complex bugs caused by the misuse of pointers or bad pointer arithmetic will no longer have to deal with this entire class of problems. In Java, you will never deal with pointers, only objects. Because of this combination of garbage collection and removal of the pointer construct, Java has generally taken the problem of memory management away from you. Consequently, programs written in Java are more reliable.
Java's unique object-oriented implementation gives it another set of desirable characteristics-simplicity and familiarity. In many ways, its syntax is similar to C and C++, thus making it familiar. On the other hand, it strips out all the programming constructs of this language that are historical artifacts contributing little to writing object-oriented programs. For example, the structure construct is removed because everything in Java is an object. Unions are removed because they make you think too much about how memory is laid out, a low-level memory consideration that Java's garbage collection and removal of pointers tries to abolish. Other procedural features of C and C++ that are removed include functions, which are replaced by class methods; preprocessor constructs, such as typedef and ifdef; the hated goto statement; and, of course, pointers. Java also replaces the complex and troublesome multiple inheritance of C++ by a simpler combination of single inheritance and interfaces. With all these features in mind, Java becomes a simpler and easier language to use.
Java has all the object-oriented features discussed in the previous section. It uses message passing to let objects communicate, and it supports dynamic binding, allowing a message to be sent to an object even though its specific type may not be known until runtime. Abstract classes and interfaces enable you to specify a design without having to worry about a specific implementation. With Java's access control constructs, you can define different levels of access to your class's methods and variables that are available to external classes.
Portability is critical to success in the emerging world of networked applications and commerce. On the Internet, you cannot make assumptions about what kind of platform your applet will run on. Not only do you have to write software that will run on the client's underlying architecture-such as 80x86, Powerpc, or 68000 series-but you must also ensure it will have the correct look and feel of the native interface. To make things even tougher, this software needs to be fast and compact.
Java takes a multipronged approach to this challenge. At the heart of this approach is the fact that the Java compiler generates bytecodes that are interpreted at runtime. The fact that bytecodes are generated is important because it avoids the problem of basing the binary code on a basic set of primitive types-such as integers and floating point-that would be tied to a specific platform. For example, even something as simple as an integer data type is implemented in a different manner on different platforms (16-bit on some, 32-bit on others). Java defines its own set of primitive data types and reconstructs large 16-bit or 32-bit values at runtime by composing them out of individual bytecodes. Java consistently uses the big-endian format to do this, thus avoiding another portability conflict about how platforms store primitive data types. You can also quickly and efficiently convert the bytecodes at runtime to the underlying native formats if necessary. In short, Java's underlying encoding scheme is architecture-neutral.
The bytecode-based system is important to writing a portable interpreter. The bytecodes generated by the compiler are based on the specification of a Java Virtual Machine, which, as its name suggests, is not a specific hardware platform but a machine implemented in software. The virtual machine is very similar to a real CPU with its own instruction set, storage formats, and registers. Since it is written in software, however, it is portable. All that's needed to take Java code compiled on one platform and run it on another is to provide a Java interpreter and runtime environment. The runtime system is written in an easily portable fashion. Once you have this system, everything becomes easy. You don't even have to port the Java compiler-it is written in Java!
Another advantage of the Java interpreter is that it improves software development by eliminating the link phase of the development process. You can go straight from compiling your code to executing it. Linking actually occurs at runtime through mechanisms discussed in the section "Java as a Secure Environment."
A couple of higher-level features also improve Java's portability. The Abstract Window Toolkit (or AWT) is constructed to be a visual interface that is portable across platforms. This way your applets will look good regardless of the underlying interface. They will have the proper "look and feel" on Microsoft Windows, the Macintosh, or X-Windows. Although not a portability issue in the strict sense, Java stores characters by using the 16-bit Unicode format as opposed to the 8-bit ASCII standard. Unicode is used if you need to support international character sets. Since the Internet is an international network, this consideration is important.
One of the reasons why Java's portable solution is such a coup is that interpreted platforms have generally been very slow. Often their performance is so poor that systems based on these interpreters have been unusable. Java's bytecode system, however, provides a "lean and mean" interpreted solution. It isn't based on multimegabyte executable "image" files. Rather, the runtime system works with small binary class files compiled to Java virtual machine code. These files typically have a size of only a few kilobytes.
However, Java offers more than just the byte-code system to get high performance. Since the byte-code system is at a low level, it can be easily converted at runtime to the native platform. Just-in-time Java compilers will be out in the near future to allow this. Another performance enhancement is a by-product of another Java feature. When a class is brought into memory at runtime, it runs through a verification process as part of the Java security mechanism (discussed in the section "Java as a Secure Environment"). This process not only guarantees that the code you are loading is secure, but the end result is code that requires less runtime checks. These checks, which otherwise would hurt performance, now don't need to be executed. For example, the runtime system does not have to check for stack overflows on verified code.
One of the key features that Java offers to improve performance is multithreading. Most interactive applications are characterized by large periods of time during which the user pauses between actions to decide what to do next. In traditional single-threaded environments, the application may sit idle during such periods. In multithreaded environments, however, the application can perform other tasks during these types of delays or at any other time. This is critical for network applications, which can take long periods of time to load files. Wouldn't it be more efficient if you could read the current page of text while the next page is being downloaded? Multithreading also greatly improves the usability of multimedia applications. For example, you need to be able to interact with your system while a sound or a movie is playing. Multithreading is useful for even more down-to-earth things, such as running a background thread that spell checks your document as you are typing in words.
You can use Java's easy-to-use multithreading environment to perform all kinds of optimizations to your program. In fact, the designers of Java have taken great pains to make sure it's easy to write multithreaded programs. Historically, writing multithreaded applications has been quite difficult-a key reason they have appeared infrequently. Furthermore, Java uses threads to improve its own performance. The garbage collector that takes care of your memory management concerns runs in the background as a low-priority thread. So even when your program is doing nothing, the garbage collector could be busy optimizing memory!
There are some other interesting things Java does to guarantee good performance. As stated earlier, Java is "objects all the way down." In some object-oriented systems, primitives such as integers and floating point numbers are implemented only as objects. So to perform an operation such as adding two numbers, you actually have to call an object method. If you are doing some serious number crunching, this will absolutely kill your performance. Java has an intermediate solution to resolve the dilemma between having good performance and being a pure object-oriented system. It implements "type wrappers" to take primitives, such as numbers, and make them appear as objects. This way, methods-such as converting numbers to strings-can be performed by using the object-oriented method style. However, if you need to work with raw data types, you can use the primitive formats to produce such things as CPU-intensive images like fractals. Java can even be used to create the computation-hungry Mandelbrot set. It's rather remarkable that Java can do this; if you don't believe it can, then see Chapter 14, "Advanced Image Processing."
If you really need that final push to your performance, Java can link to executable code written in a low-level native language such as C. Java code that does this is said to use native methods. This technique can also be used to implement platform-specific features. However, using native methods is not without its drawbacks; it will probably compromise the portability of your code. Fortunately, Java performance is generally good enough that you don't need to bring in native methods very often.
Finally, it is important to remember that the Java community is constantly working on techniques to improve performance but not compromise all the other features of Java, such as portability. The just-in-time compilers that will be out later this year are among the most impressive of such optimizations.
Much of what has been discussed so far makes Java well suited for network computing. In particular, the lightweight nature of the binary class files makes it possible to download Java code without serious performance hits. Multithreading is also critical for latency-laden network operations; your applet can download code and resources in the background while the end user is deciding the next action to take. Portability frees you from knowing how your applet will run and what it will look like on your potential client's unknown platform.
Another feature of Java that makes it excellent for distributed computing is that it's dynamic. Since there is no linking phase after compilation, the resolution of references is put off until runtime. Java also does not calculate the layout of objects in memory until they are used at runtime. Consequently, if you add a new method to one class, you don't have to recompile another class that uses the class but not the method. In C++, you are constantly having to recompile multiple classes because one is "dependent" on the other-this is known as the "fragile superclass problem." In its typical efficient manner, Java solves the superclass problem while taking care of other issues. Since the Java dynamic system removes difficulties caused by unnecessary dependencies, it's easy to download a special subclass to handle a specific situation without worrying about "linking" problems.
The Java development package also gives you a variety of good communication constructs. The message-passing mechanism of its objects can be used to let two different applets communicate; this is known as "inter-applet communication." Sockets, pipes, and thread synchronization constructs also provide other easy-to-use communication mechanisms. In short, Java provides all the basics for creating distributed systems.
Although distributed systems are extremely powerful, they open the door to a range of security problems, which are particularly serious on a vast open network like the Internet. For normal data transmission, you have to be concerned with someone eavesdropping on your information as it passes through the network. If you have a server on the Internet, you have to worry about someone breaking in and wreaking havoc on your internal network. And if you're downloading applications from the net, you have to worry about it causing damage to your host system.
The kind of damage a poorly written application could inflict on your host spans a range of extremes. On one hand, a poorly written application can misuse the resources of your system, taking resources you might need elsewhere. For example, a C program that mismanages memory can simply take memory that another one of your applications needs. If the program is really bad, it can cause your system to lock up. Fortunately, it's usually easy to recover from such problems. However, the other end of the extreme is not quite so innocent. A malicious program can attack some of the most critical resources of your computer, such as your hard disk, causing damage less easily fixed. If the program is a virus, it can be even more pernicious. It can lie in wait for weeks or even months, then one day pounce on your computer and connecting network, causing hours or even days of lost work time. If this attack came from an application that you ran from the World Wide Web, you would probably be reluctant to use the Web again. Consequently, bad Web programs not only threaten your system, but the viability of the Web as a whole.
The designers of Java know just how important security is. For this reason, they built a multi-layered security system whose presence is felt throughout Java. This security model consists of four main layers:
Figure 1.1 gives an overview of the lifetime of Java code as it travels through the layers of security. Those modules related to security are set off in boldface.
Since security is such a critical part of Java, it is worth taking a moment to look at it in greater detail. In doing so, you will get some greater insight into the inner workings of Java.
Figure 1.1 : The Java life-cycle in relationship to its security layers.
You have already seen a couple of reasons why Java is a secure language. A key ingredient here is removing pointers from Java. Without pointers, a bad or malicious program cannot invade the memory space of other applications. This removes a major source of attack for viruses. For example, a threatening program cannot use Java as a launching pad to directly attack your operating system kernel. Furthermore, the absence of pointers makes the Java applet itself more secure and reliable. An applet won't crash because of a "dangling pointer," as is often the case in C and C++.
Another security feature of the language is its class access mechanism. Classes can control the kinds of access other classes have to their methods and variables. There are four access modifiers, ranging from public, which indicates availability to all classes, to private, which makes a method or variable accessible only from within the class where it's defined. These modifiers can make your class more secure by denying other classes access to critical behavior. For example, if you have a class that manages critical data in a private method, another class cannot invoke that method to change the data. Access modifiers will be discussed in more detail in the next section.
The compiler uses very strict checking to ensure adherence to these and other Java language constructs. Java is a strongly typed language, so runtime bugs aren't introduced because of freely casting one type of object to another. A language like C is notorious for its loose casting mechanism. A pointer to a structure can be fairly easily cast into a long integer, and vice versa. When such casting is done incorrectly, pernicious and difficult-to-find bugs can be introduced, but this kind of casting is eliminated in Java. All casts must be explicit, and those that don't follow the semantics of the language are disallowed. Because the compiler is so strict, many errors that would otherwise appear at runtime are caught at compilation. Since runtime bugs are potentially dangerous and can be time-consuming and difficult to track down, it's good to catch as many mistakes as possible during compilation. Java's strict compiler is one reason the environment is said to be robust.
The bytecode verifier is the most critical line of defense in the Java security system. If a rogue class can get through this layer, you could be in real trouble! However, this is unlikely. The verifier uses various techniques, including a theorem prover, to ensure that the bytecodes of the class being loaded do not violate any of the structural constraints Java places on incoming code. The verifier is, therefore, positioned to catch any potential malicious actions caused by bytecodes produced by a hostile compiler or subject to post-compilation tampering.
Verification goes through a couple of steps. The first step is to verify the format of the incoming file to make sure it is indeed a Java class and has been properly constructed. The next step is much more complex. The verifier basically sets out to prove that the code has a variety of properties. If the bytecodes make it through this phase of the verifier, then you'll know the following things:
As a result of these properties being proved, the runtime system will know a couple of other important things, the most important being no forged pointers in the code.
A beneficial side effect of the verification process is that the interpreter is free from performing many of these checks as the code is being executed. For example, it does not need to conduct expensive checks to see if a stack overflow is about to occur. Because such checks are not necessary, the interpreter will run much faster.
Another security-related step occurs when the interpreter loads the verified bytecodes. When the interpreter brings in a class, it defines the memory layout of the class. Recall that this is a feature of Java's dynamic linking used to solve the "fragile superclass" problem. Dynamic linking also has a security advantage. In traditional languages, memory is laid out at compile time. A malicious programmer, knowing the layout of memory in the executable code, could then tamper with the pointers to get around security problems. Since Java performs memory layout at runtime, however, this potential security bypass is thwarted.
After a class is verified, it's ready for runtime use. The Class Loader brings each class into a unique namespace that corresponds to its origin. The default namespace is for classes that come from the local file system. Such classes are called built-ins; they can never be replaced by a class that comes from an external source because there is a separate namespace for each network source of classes. When a class is referenced, Java looks first for a built-in class. If it isn't found, then Java inspects the namespace of the referencing class. This approach prevents network classes from replacing a built-in, or a class from one network source overriding one from another source. The security implications of this approach are subtle but important. Java tries to implement as much as possible through Java classes; for example, the Security Manager module is represented by a SecurityManager class, you get access to system resources through the System class, and, as will be seen shortly, class loader policies are implemented by ClassLoader classes. By preventing built-ins from being overridden, Java protects critical modules, such as the SecurityManager or System. It's easy to imagine what could happen if a network class was allowed to replace the SecurityManager class.
Subclasses of the ClassLoader class are used to enforce namespace policies. The Class Loader system can consist of multiple instances of ClassLoader subclasses. For example, one Class Loader can be used for classes brought from inside a firewall, but another Class Loader class can be used for those brought in from the Internet. The local file system, by default, does not use a ClassLoader class. Instead, it searches for classes in directories listed in the CLASSPATH environment variable; you can modify this path to include the directories that have your classes. Keep in mind that there's a subtle difference between the Class Loader mechanism, which applies to the entire Java runtime environment, and instances of the ClassLoader class, which implement specific policies.
Even after a chunk of bytecode has gotten past the verifier and the class loader, it is still technically in a position to cause some damage. Suppose that a class downloaded from the Internet has some code to delete files from your hard disk. This can be done legitimately by calling the delete() method of the File class and so will pass the verifier and class loader. Fortunately, the final security layer, represented by the SecurityManager class (also known as the Security Manager), will prevent this from occurring. The Security Manager is responsible for enforcing a set of policies for protecting the runtime environment from unauthorized transactions. Whenever a potentially "dangerous" action is about to happen, the Security Manager is asked for authorization to perform the action. Based on how the manager is implemented, the action may be denied or granted.
Different browsers and applet viewers can use the Security Manager in an appropriate way. Once installed into the runtime system, the Security Manager cannot be replaced. These browsers may grant levels of authorization for different actions. For example, the Netscape Navigator has a very conservative Security Manager. The most dangerous class of actions, those of reading and writing from the hard disk, are prohibited altogether. On the other hand, the HotJava browser has a more flexible configuration. It can be set up to grant full disk access from applets loaded locally, some access to applets loaded from within the firewall, and no access for those brought in over the Internet.
A wide variety of actions are subject to authorization by the Security Manager. When a class is asked to perform a potentially dangerous action, such as a file delete, it will ask the SecurityManager class for authorization. If it isn't permitted, a security exception will occur. Besides all file-related activities, the actions of the most importance to security are network accesses. Once more, restrictions are usually based on how the SecurityManager is set up, but there are a few generalities. An applet loaded over the Internet can connect only to the host from which it originated; it will not be allowed to connect to anywhere from inside the client's firewall, nor will it be permitted to use the client to act as a launching pad into some other Internet site. An applet is also prevented from running as a network server (this has some implications that are explored later). Restrictions enforced by the SecurityManager will be discussed throughout the book as the appropriate topics dictate.
You will now be guided through a very quick tour of the basics of the Java language. If you have experience with C or C++, then much will be familiar-you might want to skip over parts of this section. If you don't know these languages, don't worry. The basic mechanics of the language are easy, and you will see many examples throughout the book. Discussion of classes, methods, and objects will be postponed until the next chapter.
As stated earlier, everything in Java is an object. The partial
exception to this is the primitive data types. These data types
have a standard size across all platforms; this standardization
is a key aspect of Java's portability. Table 1.1 lists the primitive
data types.
Data Type | Size |
byte | 8-bit |
short | 16-bit |
int | 32-bit |
long | 64-bit |
float | 32-bit floating point |
double | 64-bit floating point |
char | 16-bit Unicode |
If you are a C or C++ programmer, you might have noticed a couple of things. First of all, there is no unsigned type specifier. The char data type has been replaced by the byte primitive. The char type is now 16 bits, instead of the old 8 bits, because Java bases character data on the Unicode character set. Unicode is a standard that supports international characters, thus broadening the potential base in which your application can run. Although Unicode is a much broader standard than ASCII, you will probably have many opportunities to program in the 8-bit standard. Some default class behavior and localization methods will be available for doing this. This book will focus on ASCII output.
The only primitive data type not in Table 1.1 is boolean. A boolean variable cannot be converted to a number and has only two values: true or false.
You might have noticed that these primitive data types present a partial exception to Java's pure object-oriented nature. It is "partial" because Java has a suite of classes used to encapsulate these data types as objects. These classes are called type wrappers and are discussed in Chapter 2, "Object-Oriented Development in Java."
Literals are used to represent the primitive types. Integers are defined in a manner similar to C. They can be literally set to a decimal value, such as 10. A hexadecimal value is indicated with a leading 0x; 15 is represented by 0xF. Octal values (base 8) are prefaced by 0.
Floating point numbers are represented by the standard decimal point notation, such as 3.1415. These can be stored as a 32-bit float or a 64-bit double; the latter is the default. The notation style of 6.1D or 6.1F can also be used to designate the number as a double or float, respectively.
Characters can be represented by a single character in quotes such as 'a'. Escape sequences are used to represent special characters and are preceded by a backslash (\). For example, tab is \t, newline is \n, and so forth. See your Java references for a listing of all the escape characters.
The last literal is not based on a primitive data type. Strings are represented by zero or more characters in double quotes. An example is "This is a Java book!". The literal string can also use escape characters. For example, to add a new line to the previous example you would write: "This is a Java book!\n".
String literals are implemented as objects of the String class. Operations on strings do not occur on character arrays (as in C), but through class methods; these operations are discussed in more detail in the next chapter.
Java has three types of variables: instance, class, and local. The first two types are talked about in the next chapter in the context of the discussion on classes. Local variables can be declared inside methods or blocks. Blocks are statements appearing in braces { }. Any local variable declared inside a left brace is valid until the right brace, at which point the variable goes out of scope.
Individual variables are declared in the general format:
<type> <variable name>
For example, to declare a double called pi:
double pi;
You can also asign a value to it:
double pi = 3.1415;
Variable names are prefaced by letters, an underscore, or a dollar sign. They can use most characters, including numbers. However some symbols, such as those used in operators, should not be used. For example, you should not call a variable pi+3.
Java has three comment styles. Two are similar to those used in C and C++. A double slash (//) means that everything to the end of the line should be ignored:
// Ignore this
Everything between the characters /* and */ is also ignored. This can be spread over several lines. If the first part of the comment starts with /**, then a special documenting feature is indicated. How this works is discussed in Chapter 14.
Table 1.2 lists a quick summary of operators in Java, which are fairly simple to use. The following code declares two integers, assigns values to them, and adds them to a third variable:
int x,y;
x = 3;
y = 4;
int z = x + y;
The final value is 7.
The following code applies the bitwise AND operator to the end of the previous example to get the value 4.
int q = z & 4;
You'll see examples of other categories of operators in the upcoming
sections.
Classification | Operators |
Arithmetic | + - * / % |
Relational Operators | < > >= <= == != && || ! |
Bitwise Operators | & | ^ << >> >>> ~ &= |= ^= |
Assignments | = += -= /= %= |
Bitwise Assignments | &= |= <<= >>= >>>= ^= |
Ternary Operator (if...else shorthand) | ?: |
Increment | ++ |
Decrement | -- |
Expressions that have multiple operators are resolved according
to where they are in a hierarchy of precedences, shown in Table
1.3. Operators higher up in the precedence table are evaluated
first. If multiple operators on the same line have the same precedence,
then they are resolved by left-right order. If you are confused
or cannot remember precedence, then a nice rule is "When
in doubt, use parentheses."
. [] () |
++ - ! ~ |
* / % |
+ - |
<< >> >>> |
< > <= >= |
== != |
& ^ |
| |
&& |
|| |
?: |
= and other assignments |
Bitwise Assignments |
Table 1.4 lists Java keywords; they are reserved for Java statements,
so you can't use them for things like variable names. They are
identifiers for such things as data types, conditionals, control
flow constructs, class definitions, and object implementations.
Most of these keywords will be described in this and the next
chapter. Those that are reserved but currently not implemented
are italicized.
Abstract | boolean |
break | byte |
byvalue | case |
catch | char |
class | const |
continue | default |
do | double |
else | extends |
false | final |
finally | float |
for | goto |
if | implements |
import | instanceof |
int | interface |
long | native |
new | null |
package | private |
protected | public |
return | short |
static | super |
switch | synchronized |
this | threadsafe |
throw | transient |
true | try |
void | while |
An if...else conditional is used to execute code based on whether a boolean test is true. Single statements can follow, or multiple statements can be declared in braces:
if (test == true)
// ... A single statement to execute if test if true
else {
// ... multiple statements to execute if test if false
}
The test must return a boolean value. This means an expression such as
if (1)
is not valid because it returns a numeric, but not a boolean. It's acceptable to nest if...else statements or call them in a nested series, such as if...else...if...else...if...else.
The latter can also be simulated with the switch conditional. Not being restricted to just boolean comparisons, the switch conditional uses a comparison with a general primitive to conduct its test. The following code uses an integer to perform its test:
switch ( Count ) {
case 1:
// ... test 1
break;
case 4: {
// ... do some operation...
return 0;
}
case 24: break;
default:
return -1;
}
The case statements can appear in braces.
Java has three looping operations. The for construct loop is structured as follows:
for (init; test; post-test)
The first part of the expression is any initialization at the start of the loop. The test is a simple or complex expression. The last part is an expression such as an increment or decrement of a variable. The for loop is followed by a single statement or a block of code. For example,
for (k = 0; k < 10; ++k) {
// ... do something
}
loops ten times.
A while loop is just the test portion of the for loop:
while (test)
The do...while construct performs the test and the end of the loop:
do {
// ... do something
} while (test);
The break construct is used to exit from a loop, and the continue statement skips to the next iteration of the loop. Labeled loops can also be used to control where to go in complex loops. If a label follows a break statement, then the code breaks out of the nearest loop that has the matching label. The following example effectively quits both loops when the variable j is greater than 100:
int i,j;
quit: for (i = j = 0; i < 100; ++i) {
for (int k = 0; k < 10; ++k) {
// ... Do something
if (j > 100)
break quit;
}
}
If the break statement in this example was replaced by the following,
if (j > 100)
continue quit;
then the code would jump to the next iteration of the outer for loop.
Arrays are first-class objects in Java; they are not just a pointer to memory as in C. Consequently, Java arrays are a lot safer. You cannot indiscriminately assign an element to just any index in an array. Java makes sure the index is valid-this prevents the difficult-to-track memory access violations that occur so easily in C. If you try to access a bad array index in Java, an exception will be thrown and no action will be taken.
Because Java arrays are objects, their semantics are a little different from their counterparts in C or C++. The thing that confuses most new Java programmers is how to allocate a new array. First, you cannot declare an array with a prefixed size; it must be declared as an uninitialized variable:
int numbers[]; // For integer arrays
String myStrings[]; // For arrays of String objects
Another way to declare array variables is to put the braces after the type. Some programmers consider this method more readable.
String[] myStrings;
As you might have noticed from these examples, arrays can be used for both primitive data types and classes.
The next step is to create the array by using the new operator to construct an object instance; this will be discussed in more detail in the next chapter.
int numbers[] = new int[5]; // For integer arrays of length 5
String myStrings[] = new String[20]; // For arrays of
length 20 of String objects
These examples create an integer array of 5 elements and a String array of 20 elements. However, the arrays still don't actually contain anything. Each slot is initialized to a default value. Integers are all set to 0, and String elements are all set to null (indicating no object).
It's easy to add elements to the array after it's created, much as you do in C++. It is a zero-based system with integer indexes. Therefore, to assign the first element you would call the following:
myStrings[0] = "My First String"
numbers[0] = 10;
If you try to make an invalid assignment to an invalid index, an ArrayIndexOutOfBoundsException will be thrown:
numbers[10] = 10;
Chapter 2 covers how exceptions are handled; for now, you just need to know that such exceptions occur at runtime. The preceding error will not be caught by the compiler.
A public instance variable called length is used to get the size of an array:
int q = numbers.length; // This is 5
Array sizes are final; any attempts to change the length variable will lead to problems.
Java doesn't support multidimensional arrays. However, they can be effectively simulated by using arrays of arrays, which can be initialized in several different ways. The simplest way is to define the array with preset sizes:
int k[][] = new int[5][4];
k[1][3] = 999; // Assign a value to it
This example creates a 5 ¥ 4 array of arrays assigned to the variable k. The second line shows that assigning an element to the array is straightforward, much like C or C++.
Another way to create a multidimensional array is to declare and then later assign its sizes:
int z[][];
int outerSize = 5;
int innerSize = 4;
z = new int[outerSize][innerSize];
Not all the dimensions need to be known at the time of allocation-only one dimension is required:
int j[][] = new int[5][];
j[0] = new int[4];
j[1] = new int[4];
In this example, the outer array is set to size 5, then two of its elements are set to arrays of size 4. It is interesting to note that the statement
j.length
will return size 5, but
j[0].length
will return size 4.
Java can work with two kinds of applications: Applets, as stated at the beginning of this chapter, are programs executed as part of a Web page and displayed with a Java-enabled browser; standalone applications, on the other hand, are general-purpose Java applications that don't need a browser to run. Although applets and standalone applications are compiled in a similar fashion, they are created differently.
Listing 1.1 shows the code of a minimum applet, which is stored in a file called FirstApplet.java on this book's CD-ROM. It is compiled with the following command line call to the Java compiler:
javac FirstApplet.java
The output from the compiler consists of Java bytecodes in a file called FirstApplet.class. This class code is ready to be directly run from the interpreter-no further steps are necessary.
The code consists of one class called FirstApplet, a subclass of the Applet class. The Applet class is used to tie the applet into the browser environment; it will be described in more detail in Chapter 6, "Building a Catalog Applet."
Listing 1.1. An applet that compiles but does nothing.
import java.applet.Applet;
public class FirstApplet extends Applet {
}
To run the applet in the browser, you need to create an HTML file.
Listing 1.2 shows the HTML text needed to run the applet. Note
the second line of the listing: The <APPLET> tag is a special
HTML extension used to launch Java applets; notice especially
the CODE attribute. The value
assigned to it indicates the class that will be run. In this case,
it is the FirstApplet class that was just compiled. The <WIDTH>
and <HEIGHT> tags are used to specify the bounding area
of the applet, so you can control what the applet looks like on
your Web page. How Applet tags are used in HTML files is discussed
in more detail later in Chapter 6.
Listing 1.2. The HTML text to run the applet.
<TITLE>First Applet</TITLE>
<APPLET CODE="FirstApplet" WIDTH=300 HEIGHT=200>
</APPLET>
The program called appletviewer is used to run a Java applet from outside a browser. It's limited in capability-it lacks all the features that a browser has-but it's good for rapid development. You can launch an applet from appletviewer by passing it the name of the HTML file that starts the applet on the command line:
appletviewer FirstApplet.html
Note |
Most of the projects and examples in the book can be run by loading the HTML in the example directory of the CD-ROM into the appletviewer program or a browser such as Netscape. When there are additional steps required, such as in the Part IV server programs, you should look at the README file in the working directory of the CD-ROM. |
Listing 1.3 shows the code of a simple standalone application, which is stored in a file called Standalone.java on this book's CD-ROM. Like the preceding sample applet, and class files in general, it's compiled with the program javac.
The code in a standalone application differs from an applet's code in two ways: first, standalone applications do not need an instance of the Applet class; second,they are started by a call to the main() method, as in C programs. The main() method is the first routine executed in the program, which you start by invoking the Java interpreter:
java Standalone
The .class extension should not be specified as part of the main parameter. Additional parameters can be specified after this; they are passed to the main() method as an array of String objects, as the example shows.
The example just prints text to the standard output console by using a call to the println() method of the System class: System.out.println. You will see this method invoked regularly throughout the book-it provides the same role for Java programmers as printf() does for C programmers. It will be used for both debugging and conveying program information.
Listing 1.3. A simple standalone application.
import java.lang.System;
public class Standalone {
public static void main(String args[]) {
System.out.println("Running standalone applet!");
}
}
Most classes and methods can be used in both applets (which are tied to browsers) and standalone applications (which are not). Using a browser gives your applet access to a range of services, such as linking your applet to another Web page or using browser resources like the status bar. On the other hand, much of what you can do in an applet is restricted by a browser's security constraints. You don't have as much freedom to perform file and network operations as you do in a standalone application.
Sometimes the security restrictions of a browser force you to write a standalone application, as illustrated in the chapter on network servers in Part IV of this book. Such servers generally need to be written as standalone applications. Of course, security is both a blessing and a curse. It's a curse because it limits what you can do, but of course, if someone used that lax security to break into your system, you could end up with quite another view.
Consequently, there may come a time when people begin to write their own Security Man-agers into standalone programs to fit special needs. For example, a corporate "intranet" is a possibility.
This book, as its title indicates, will focus on applets. However, some features, such as network servers, are best created as standalone applications, so these will be used when appropriate.
Java is more than just an Internet programming language. It is a full-featured, object-oriented language in its own right. As the language matures, Java may be considered the language of choice for all object-oriented programming. In this way, Java might compete with major object-oriented languages, such as C++ or Smalltalk. It is entirely possible that corporations and other institutions will use Java for in-house applications, such as timesheets and client/server projects. Java will probably prove to be especially popular on intranets. So despite its billing as primarily an Internet language, Java's wide variety of features could establish it as the next major language in general, for uses ranging from in-house projects to commercial uses to the Internet and intranets to being the first language taught in universities. For this reason, Java is worth knowing, regardless of how you use the Internet.
Chapter 2 moves from this overview of Java's foundations to a deeper exploration of its object-oriented features. You will also be introduced to the Java Development Kit (JDK), which offers a large suite of classes for creating your applets. Techniques for writing more readable, efficient, and extendible code will also be discussed.