Java Secrets
Java Secrets
Preface
About the Author
Appendix A
Appendix B
Appendix C
Appendix D
Appendix E
Appendix F
Appendix G
Appendix H
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
Table of Contents
Preface
T here are more than 100 books about Java on bookstore shelves today, and at least 90 of them are completely predictable and
more or less interchangeable. It’s as if they had all been written from the same outline but by different authors.
Each book begins with a chapter about what’s special about Java and how it differs from other programming languages. Each book
shows how to write Hello World and other command-line applications to teach Java’s syntax. There is a chapter or two on object-
oriented programming, a chapter on threads, a chapter on exceptions, and a few chapters on the AWT. I know. I wrote one of these
books.
This book is different. It starts where the other books stop. This book assumes that you already know Java’s syntax and what an
object is. This book assumes that you’re comfortable with the AWT. Instead of rehashing these topics, this book delves into the parts
of Java that are not documented by Sun, that are not generally accessible to anyone with a Web browser, and that are not already in a
hundred other books.
I had some reservations about writing this book. I still do. This is a dangerous book. It reveals knowledge that can easily be abused.
Improper use of the secrets revealed herein can easily tie Java programs to specific platforms or implementations. As a longtime Mac
user, I know the agony of watching all the best software come out on Windows first and the Mac much later, if at all. I do not want to
extend this trend to Java-based software.
Nonetheless, I have come to the conclusion that a book like this is necessary if Java is to move out of its niche of creating applets for
Web pages and into the broader software development market. There are many applications for which Java is ideal, but which cannot
be written without more information than Sun has chosen to reveal. These include stand-alone executable applications. HotJava and
javac are stand alone applications, so it must be possible to write them, but until now, Sun has not revealed how. This book reveals
that secret among others.
There are other reasons programmers want to know these details. Just as in the early days of DOS when you needed to use
undocumented functions to load a program without executing it so you could write a debugger, so too will you need to use
undocumented parts of Java if you’re working on development or runtime environments.
However, rationalize though I might (and I’m quite good at rationalizing, I admit), the real reason this book is being written is that it
seemed like a neat thing to do at the time. This is far and away the most exciting book I’ve ever written. The sheer number of “Aha!”
experiences I’ve had while researching and writing it is phenomenal. I hope you’ll get the same feeling while reading it. I know the
information I present here will be misused. I accept that. Nonetheless, I firmly believe that in the long run, more knowledge is a good
thing, dangerous though it may be; and that secrets are meant to be revealed.
There are three different ways a Java program can become dangerous. It can rely on the internal structure of Java objects; it can use
classes it isn’t supposed to know about; or it can be platform-specific. This book covers all three.
Part I: How Java Works
After a brief introduction, Part I begins with six chapters on Java internals. You learn how objects and primitive data types are laid
out in memory, how arguments are passed to and values returned from methods, what a variable really is, and more. Java’s
implementation of arrays and strings will be explored. Different possible models for threads and algorithms for garbage collection
are discussed and compared, shedding some light on why Java uses the data structures and algorithms it does and why it sometimes
behaves in unexpected ways. This is all tied to the Java .class file format in Chapter 5, where you learn how to read and disassemble
Java byte code. You also learn some details about Java’s thread model and garbage collection algorithms.
Finally, you learn how an applet runs and what really happens when a Web browser loads an applet.This section is dangerous
because none of it is guaranteed. Tomorrow, Sun could change Java’s thread model from cooperative to preemptive or make strings
null-terminated. Worse yet, Java might be implemented one way on one system and another way on another. Writing code that
depends on implementation issues is always dangerous but sometimes necessary.
Nonetheless, it often helps to know what’s going inside a class or method even if you don’t explicitly use that information. For
example, knowing whether the Vector class is implemented with a growable array or a linked list has a lot to do with whether you
choose to use it in a program that performs thousands of insertions in the middle of a list. You can drive a car without knowing the
first thing about carburetors or transmissions, but it certainly doesn’t hurt to know about them, especially when things go wrong.
Knowing what goes on the under the hood but ignoring it when it isn’t relevant is a good technique for both programmers and
drivers. Not knowing isn’t.
Some may object that this goes against the philosophy of object-oriented programming. Objects are supposed to be black boxes into
which data is sent and out of which a result flows. You aren’t supposed to need to know what happens inside the box. However,
objects aren’t everything, and practical experience shows that time and time again, the black box doesn’t do exactly what it’s
supposed to and you need to open it up and fix it. Part I opens up many black boxes to expose their inner workings.
Part II delves into the sun classes, a group of undocumented packages that add considerable power to Java programs. The following
are just a few of the undocumented classes that will be covered in this section:
! More LayoutManagers
! Communicating with ftp, mail and news servers
! Data encoding and decoding
! Character set conversion
! Protocol and content handlers
As you can see, Sun has hidden a lot of functionality inside the Sun classes. This book reveals it.
Part II is dangerous because these classes may not be present in future releases of Java. They may not even be present in Java
implementations not written by Sun. If they are present, their public methods may not have the same signatures. Nonetheless, they
provide too much additional power to be ignored, and there are some very simple techniques that allow one to use these packages
safely in even non-conforming implementations.
Part III explores the possibilities opened by platform-dependent code. It demonstrates how to call the native API and how to create
stand-alone executable programs.
This part is dangerous because it limits the audience of a program. It’s also dangerous because it violates many of the security
restrictions normally imposed on Java programs. Nonetheless, not all programs are applets on Web pages. Many programs can
benefit from taking advantage of native code, either for speed or to add additional functionality not present in the AWT. There are
ways to use platform-dependent code to enhance your application without making your program inaccessible to users on all other
platforms. This section will explore these possibilities.
You’ll notice some special icons sprinkled throughout this book to draw your attention to the information at hand. The following
briefly describes the use of these icons:
Note: This icon alerts you to information that, for one reason or another, is undocumented or is not common knowledge.
This information can contain time-saving tricks and techniques or nifty facts that will enhance your understanding and
learning of Java.
This is not an introductory book. It is for the programmer who has learned enough about Java to be frustrated by its limitations. You
should have a solid grasp of the fundamentals of both the Java language and the AWT, including advanced topics like threads.
Although every effort has been made to make this book accessible to as broad a range of readers as possible, this is not an
introductory book and does require more of its reader than most books on the market.
On the other hand, this book does not assume prior experience with assembly language, Java byte code, compiler design, or even
pointers. In fact, this book may serve as a first taste of some of these to a reader who’s never seen them before, in Java or any other
language. Nonetheless, low-level programmers who are familiar with pointers, assembly language and compiler design should find
the discussion of Java’s implementation of these topics to be useful. They’ll simply find the book easier going than a programmer
encountering these topics for the first time.
As mentioned earlier, this book is broken into four main parts. I recommend that you begin by reading or at least skimming Part I
more or less in its entirety. This section introduces many deep concepts you’ll need later and that the rest of the book depends on.
These include bit-shift operators, Unicode, the nature of strings, the virtual machine, the class file format, and Java byte code. These
are the tools you’ll need to understand the internals of Java.The remainder of the book (Chapters 6 through 20) can be read in pretty
much any order that interests you. As a general rule, these chapters are pretty much independent of each other. While each chapter
should probably be read from start to finish, the chapters themselves are mostly self-contained.
Bugs
This book is so far out on the bleeding edge, I’ve got a personal account rep at the New York Blood Bank. I’ve done my best to try to
provide useful and accurate information. All the code in his book has been verified on at least one virtual machine (VM). Most of the
code has been tested on two or more. However, because Java runs on so many different platforms and because it is changing in
Internet time, it is impossible to be completely precise and accurate in all instances. Furthermore, precisely because the material in
this book is secret, it’s been extremely hard to verify.
Please use this information carefully and read it with a critical eye. If you do find mistakes or inaccuracies, let me know by sending e-
mail to [email protected], and I’ll correct them in future editions. I will also post corrections and updates on my Web
site at http:// sunsite.unc.edu/javafaq/secrets/, so you may wish to look there first before sending me e-mail. When you communicate
with me about a problem you’ve found, please let me know the VM, version of Java, vendor, processor, and operating system you’re
testing with. By early 1997, there were already more than 100 slightly different virtual machines is use, so it’s important to be as
precise as possible.
Elliotte Rusty Harold
[email protected]
http://sunsite.unc.edu/javafaq/
Table of Contents
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
Table of Contents
Elliotte Rusty Harold is an internationally respected writer, programmer, and educator, both on and off the Internet. He got his start
by writing FAQ lists for the Macintosh newsgroups on Usenet and has since branched out into books, Web sites, and newsletters.
He’s currently fascinated by Java, a preoccupation which is beginning to consume his life. He lectures about Java at Polytechnic
University in Brooklyn, and his Cafe Au Lait Web site at http://sunsite.unc.edu/javafaq/ has become one of the most popular
independent Java sites on the Internet.
Elliotte is originally from New Orleans, where he returns periodically in search of a decent bowl of gumbo. He currently resides in
New York City’s East Village with his wife Beth and cat Charm (named after the quark). When not writing about Java, he enjoys
working on genealogy, mathematics, and quantum mechanics. His previous books are The Java Developer’s Resource from Prentice
Hall and Java Network Programming from O’Reilly & Associates.
Dedication
To my parents.
Acknowledgments
Many people were involved in the production of this book. Andrew Schulman’s Undocumented DOS and Undocumented Windows
inspired me to write this book in the first place, and Andrew’s comments on the early proposals and outlines were extremely helpful.
My editors, John Osborn, Nancy Stevenson, Sundar Rajan, and Faithe Wempen, all provided important assistance at various stages
of development. My agent, David Rogelberg, convinced me it was possible to make a living writing computer books instead of
writing code in a cubicle. All these people deserve much thanks and credit. Finally, I’d like to save the largest thanks for my wife,
Beth — without her support and assistance, this book would never have happened.
Table of Contents
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
Part I
How Java Works
Chapter 1
Introducing Java SECRETS
T here are close to a hundred books about Java program-ming on bookstore shelves today, and at least 70 of them are completely
predictable and more or less interchangeable. It’s as if they had all been written from the same outline but by different authors. Each
book begins with a chapter about what’s special about Java and how it differs from other programming languages. Each book shows
how to write Hello World and other character mode applications to teach Java’s syntax. There is a chapter or two on object-oriented
programming, a chapter on threads, a chapter on exceptions, and a few chapters on the AWT.
This book is different. It starts where the other books stop. This book assumes you already know Java’s syntax and object-oriented
programming basics. This book assumes that you’re comfortable with the AWT. Instead of rehashing these topics, this book delves
into the parts of Java that are not documented by Sun, that are not generally accessible to anyone with a Web browser, and that are
not already covered in a hundred other books.
I had some reservations about writing this book. I still do. This is a dangerous book. It reveals knowledge that can easily be abused.
Improper use of the secrets revealed herein can easily tie Java programs to specific platforms or implementations. As a longtime Mac
user, I know the agony of watching all the best software come out on Windows first and the Mac much later, if at all. I do not want to
extend this trend to Java-based software.
Nonetheless, I have come to the conclusion that a book like this is necessary if Java is to move out of its niche of creating applets for
Web pages and into the broader software development market. There are many applications for which Java is ideal, but that cannot
be written without more information than Sun has chosen to reveal. Among other things, this includes stand-alone executable
applications. HotJava and javac are such applications, so it must be possible to write them, but until now Sun has not revealed how.
This book reveals that secret, among others.
There are other reasons why programmers need to know these details. For example, a programmer writing development tools
requires a much deeper understanding of Java’s internals than does the average application developer. Programmers merely writing
applets don’t need to know exactly how and when the ScreenUpdater thread calls the various paint() and update() methods in
different components and containers. A programmer adding applet support to a Web browser, however, absolutely has to understand
this.
Rationalize though I might, however (and I’m quite good at rationalizing, I admit), the real reason why I am writing this book is that
it seems like a neat thing to do. I know that the information I present here will be misused. I accept that. Nonetheless, I firmly believe
that, in the long run, more knowledge is a good thing, dangerous though it may be, and that secrets are meant to be revealed.
After a brief introduction, Part I begins with seven chapters on Java internals. You will learn how objects and primitive data types are
laid out in memory, how arguments are passed to and values returned from methods, what a variable really is, and more. Java’s
implementation of arrays and strings will be explored. I will discuss and compare different possible models for threads and
algorithms for garbage collection, shedding some light on why Java uses the data structures and algorithms that it does and why it
sometimes behaves in unexpected ways. You’ll learn how a Web browser loads applets and what it needs to provide for them so that
you can add applet support to your own programs. All of this is tied to the Java virtual machine and .class file format, so you’ll learn
how to read and disassemble Java byte code.
This section is dangerous because none of it is guaranteed. Tomorrow Sun could change Java’s thread model from cooperative to
preemptive or make strings null-terminated. Worse yet, it might be one way on one system and another way on another. (In fact, in
the case of threading this is already true.) Writing code that depends on implementation issues is always dangerous but sometimes
necessary. And it often helps to know what’s going on inside a class or method even if you don’t explicitly use that information. For
example, knowing whether the Vector class is implemented with a growable array or a linked list influences whether or not you
would use it in a program that will perform thousands of insertions in the middle of a list.
You can drive a car without knowing the first thing about carburetors or transmissions, but it certainly doesn’t hurt to know about
them, especially when things go wrong. Knowing what goes on under the hood, but ignoring it when it isn’t relevant, is a good
technique for both programmers and drivers; it is a very different technique from not knowing at all.
Some may object that this technique goes against the philosophy of object-oriented programming. Objects are supposed to be black
boxes into which data is sent and out of which a result flows. You aren’t supposed to need to know what happens inside the box.
Objects aren’t everything, however, and practical experience shows that sometimes the black box doesn’t do exactly what it’s
supposed to, and you need to open it up and fix it. Part I opens up many black boxes to expose their inner workings.
Part II delves into the sun packages, a group of undocumented classes that add considerable power to Java programs. The java
packages provide the public API that most programmers use, but the sun packages work behind the scenes. Many of Sun’s Java
development tools, like javac and the appletviewer, are built from Sun classes. Furthermore, many of the public classes and
interfaces in the JDK privately use the sun classes.
The following are just a few of the capabilities hidden inside the sun classes:
• Running applets
• Communicating with ftp, mail, Web and news servers
• Data encoding and decoding
• Playing audio files
As you can see, Sun has hidden a lot of functionality inside the sun packages. This book reveals it.
Part II is dangerous because these classes may not be present in future releases of Java. They may not even be present in Java
implementations not written by Sun. Even if they are present, their public methods may not have the same signatures. Classes that
are public in one version may have only package access in the future. They may even move from one package to another.
Nonetheless, these classes provide too much additional power to be ignored, and there are some very simple techniques provided
here that enable you to use these packages safely in even non-conforming implementations.
Part III explores the possibilities opened by platform-dependent code. It demonstrates how to call the native API and how to create
stand-alone executable programs that take advantage of unique abilities of the local platform.
This part is dangerous because it limits the audience of a program. It’s also dangerous because it violates many of the security
restrictions normally imposed on Java programs. Nonetheless, not all programs are applets on Web pages. Many programs can
benefit from taking advantage of native code, either for speed or to add additional functionality not present in the AWT. There are
ways to use platform-dependent code to enhance your application without making your program inaccessible to users on all other
platforms. This section explores these possibilities.
Relying on implementation-specific details opens up the possibility that your programs may stop working when Sun revises Java.
Using the sun packages means that not all Java environments may be able to run your programs. Using native code limits your
audience, increases the time-to-market, and makes your program buggier. There are ways you can limit these bad effects, but they
are real, and they must be considered. Given these problems, why would anyone want to learn about the material in this book? I can
think of several reasons.
Broader applicability
In some cases, the design of Java limits you to a very small portion of the programs you might want to write. For example, the
getAudioClip() and loop() methods of the Applet and AppletContext classes let your applets play sounds. Only applets can play
sounds, however. In fact, it’s a little worse than that. Only applets that run in a Web browser or an applet viewer can play sounds.
Applet subclasses that you instantiate in main() or embed in your own programs cannot play sounds because they don’t have an
AppletContext.
Secret: There really isn’t any good reason for restricting sound playing to applets. Applications of all sorts often need to
play sounds. By using the sun.audio classes, you can play sounds in all your Java programs, not just your applets.
More power
Secret: The sun packages let you do things you just can’t do otherwise. For example, suppose you wanted to build an IDE
for Java development and actually use Java. To accomplish this, you need to compile files, debug them, and run them. All
the hooks to do that are in the undocumented sun.tools package.
Inspiration
The sun classes are often a fertile source of ideas. Although many of the classes and packages are incomplete, they often provide a
pattern on which you can model your own, more functional classes.
Secret: For example, the sun.net.nntp.NntpClient class lets you open groups, list the articles in the groups, request
specific articles, and post new articles. That’s useful, but some obvious methods are missing. There is no method to get a
list of all the newsgroups on the server, for example. You can use the sun.net.nntp.NntpClient class as a model for your
own NNTP class that does know how to get a list of all the available newsgroups and a lot more. Furthermore, your class
can fix some of the bugs in Sun’s NntpClient class.
Sometimes you can create your own classes by extending Sun’s; some-times you’ll copy and paste; sometimes you’ll write your own
classes from scratch using a similar API. Whichever you choose, it’s almost always easier to start with a good design and correct
some minor deficiencies than it is to design a class from scratch.
Of course, not all of Sun’s designs are good. Sometimes you can learn from the mistakes made in the sun classes so you don’t repeat
them. Not every class described in this book actually performs as advertised, and I’ll be sure to tell you when that’s so. One reason
that some classes and packages are undocumented is that they’re buggy, poorly designed, or incomplete. Learning from your
mistakes is good. Learning from someone else’s mistakes is even better. You can learn from Sun’s mistakes.
Some people have asked how I discovered this information. To be perfectly honest, it really wasn’t that hard. Sun has not gone to
particularly great lengths to hide Java’s internal structure from nosy eyes.
Until this book, there have been three main sources for information about Java internals. The first source is alpha releases. Far more
of Java’s internals tend to be exposed and documented in alpha versions than in later releases. Although many details have since
changed, the HotJava 1.0a3 release provides a broad picture of many otherwise undocumented features that remain in Java 1.1.
Alpha versions of other technologies like the Java Web Server (originally called Jeeves) and Remote Method Invocation (RMI) can
be similarly useful.
While a technology is being developed, it’s often completely open for inspection because Sun hasn’t yet decided which parts to
document and which to hold to themselves. Once the product is shipped, however, previously open classes can be closed off. This is
one reason why the alpha versions of Java still reveal a great deal of information that is otherwise unavailable. Of course, as time
passes, these alpha versions become a progressively less reliable guide to current technology, so any information garnered in this
fashion must be verified and tested. Nonetheless, an alpha version is often a useful starting point.
The second source of information is the source code itself. The JDK includes source code for all the classes in the java packages.
Furthermore, Sun freely licensed the full source code for Java 1.0 for non-commercial use, such as for education and personal
edification. Regrettably, this commendable policy of openness has been rescinded with Java 1.1. Now, source code for the sun
classes is available only to commercial licensees willing to shell out big money. Nonetheless, the available source code for the java
packages and for Java 1.0 still reveals much that is not obvious from the official documentation.
The third source of information is first-hand communication with Sun’s Java team. Regrettably, but necessarily, this access has so far
been restricted mostly to Sun commercial licensees lsuch as Netscape and Symantec. It is not reliably available to the general public.
However, many members of the Javasoft team do participate in various Java newsgroups and mailing lists and do post information
that hasn’t been revealed through more official channels. The Sun-sponsored mailing lists for unreleased products seem to be
particularly fertile sources for direct interaction with Javasoft team members. Programmers are often more loquacious about their
thoughts, ideas, and problems when they’re still looking for a solution than they are once they’ve found it.
Although these are all useful things to avail yourself of, the average programmer should hardly be expected to use these informal and
incomplete mechanisms as his or her sole source of information. This book is therefore designed to collect and organize much
information that has been previously either inaccessible or inconvenient to come by for the vast majority of Java programmers.
Prior to Version 1.1, Sun made the source code for the Java Development Kit (JDK) fairly freely available. It was not difficult to get
a source code license for personal or educational use. In addition, both Java 1.0 and 1.1 included the source code for the Java classes
in the base distribution. However, full source code for the JDK 1.1 is now available only to Java’s commercial licensees and a few,
select others such as the Linux development team. Apparently the commercial licensees were more than a little peeved that Sun was
giving away what they had paid substantial sums of money for, so Sun began restricting access to the source to make them happy.
This policy may or may not be relaxed in the future.
Nonetheless, you should get a license to the source code for whatever version you can come by, even if it’s a few releases out of
date. Some classes have changed a great deal, but many are substantially unchanged since the early alphas.
The javadoc documentation generally covers only the public and protected members of a class. This is not enough, especially when
you’re trying to do things Java’s designers didn’t mean for you to do.
In particular, methods and fields that have default or package access may in fact be relevant to your classes. These are the members
that do not have an access specifier; they are not declared as public, private, or protected. For example,
int value;
InputStream getData();
Vector tokenize();
There are probably more members with package access in the classes in the Java class library than there are members with protected
and private access combined. Methods and fields with no access specifiers are accessible only from within the package in which
they’re declared.
Note: In versions of Java before 1.1b3, although these package access members could be invoked only by other classes in
the same package, subclasses in different packages could override these methods and fields. In these cases, another
method in the package might call one of the overridden methods. The superclass’s behavior could be affected by the
change in the overridden method, so by overriding a package member, you could change how any method that used the
member behaved. Sun eventually decided this was a bug and fixed it in Java 1.1b3. This behavior is still present in virtual
machines based on Java 1.0, however, and there are times when this is the best option. For example, you see in Chapter 12
that creating new encoders and decoders requires you to override these package access methods. In fact, the class was
designed under the assumption that you would do this. Now that this is no longer possible, you have to create new classes
in the sun.misc package instead.
Why bring this up now? Because often the only way to ensure that your overriding method does not unexpectedly interfere with the
proper workings of a class is to carefully inspect the source code. By looking at the source, you can see what a method is supposed to
do and what will change if you make it do something else.
Furthermore, although applets are expressly prohibited from adding new classes to existing packages, applications are not. When you
build a stand-alone application, you can build it in such a way that you add new classes to existing sun or java packages. You can
even completely replace existing classes if you prefer to use one of your own devising. It’s better to avoid this if you can, but there
are times when you have no other choice. In Java 1.0, the only way to do a reverse lookup on an IP address (that is, to convert a
dotted quad address like 204.178.32.1 into an Internet hostname like utopia.poly.edu) was to add a new package to the java.net
package. You should try to avoid doing this if you can, but when you have to do it, you might as well do it right. The more you know
about the internal workings of the classes with which you’ll be interfacing, the less likely you are to unintentionally break something
else.
Note: The bug that required this hack is fixed in Java 1.1.
Nonetheless, I’ve chosen to focus on Java roughly as found in Sun’s JDK 1.1. I’ve chosen to focus on Sun’s JDK because it is the
most widely distributed implementation of Java, especially when all of the other Java environments derived from it are factored in.
Netscape’s Java virtual machine is based on Sun source code, for example. Even virtual machines that were written independently
such as Roaster Technologies’ Roaster VM for the MacOS use Sun’s JDK class library with a few modifications.
I’m focusing on Version 1.1 because it’s the most current version of Java at the time of this writing. Even though right now 1.0.2
VMs are built into most Java-aware browsers, the life span of Java software is considerably shorter than the life span of Java books.
The only way to avoid getting drowned by the fast-moving Java tidal wave is to stay as close to the front of the wave as possible. I
expect that by the time you’re reading this, Java 1.1 will be in common use everywhere, and I hope that any further developments
won’t make too much of what’s described here obsolete.
During the four months I spent writing this book, Sun’s Java JDK went through three beta releases and two release versions (1.0.2,
1.1b1, 1.1b2, 1.1b3, and 1.1), and this doesn’t even count any of the many releases by third parties. This has given me some practical
experience at gauging what is and what is not likely to change. Surprisingly, I’ve discovered that the undocumented parts of Java
change far more slowly than the documented parts.
Between 1.0.2 and 1.1, the .class file format and the virtual machine barely changed at all. The sun packages were expanded and
some bugs were fixed, but very few classes were deleted or changed in incompatible ways. To the best of my knowledge, nothing in
the sun classes was marked as deprecated, compared to many extremely common methods like action() and readLine() in the java
packages. In fact, some of the sun classes appear to have been unchanged, aside from recompilations, since the 1.0 pre-beta. They
probably would have lasted even longer had not changes to the language specification between alpha and beta broken more or less all
of the existing source code. It does not appear that anybody is actively working on many of these classes. It therefore seems unlikely
that they’ll change suddenly and unexpectedly.
Some Objections
Even the idea of Java SECRETS disturbs some people. Java is not supposed to need a book of secrets. It is supposed to be a truly
open system that anyone can implement from freely available, well-documented specifications. It is supposed to provide everything a
programmer needs. This is only partially true, however.
If I had to pick one issue in this book that I think is most controversial, it would be platform-dependent applications. From what I
read on the newsgroups and in the press, I think that many programmers agree with me. As you’ll see in Part III, I myself am quite
torn about the whole idea.
Much of this concern is misguided, however. If Java is to fulfill its promise as a full-powered environment for developing
applications, then it cannot be hobbled by requirements that are intended for applets on Web pages. Only by taking advantage of
undocumented packages and the native API can Java programmers level the playing field with their C and C++ counterparts and
produce commercial-quality applications.
The advent of Java-based network computers only extends the problem. On a network computer, anything you want to do must be
done in Java. You cannot drop out to a native method in C. Therefore it is even more important to have full access to all the
capabilities of Java.
What went wrong? What happened to the dream of applets moving transparently and easily between platforms? The answer is that
Java succeeded. In fact, it succeeded wildly, much faster and far beyond the expectations of its designers. What was a simple
language for consumer electronics has become the most rapidly adopted programming language in history. It is being used for
applets on Web pages, for database front ends, for numerical analysis, for multi-player networked games, and for much, much more.
It is no wonder that many of these programmers need capabilities and knowledge that were not originally planned for Java.
A question may occur to the inquiring mind: If these classes and methods aren’t documented, is there perhaps a good reason for that?
Maybe these are things human beings were not meant to know.
Poppycock. Given the relatively few people working on Java, especially in its early days, combined with the large size of the API,
it’s surprising that there aren’t more undocumented features. Indeed, there are many methods in the allegedly documented Java
packages that literally qualify as undoc-umented by virtue of their poor documentation, but they are not discussed in this book.
There are four main reasons that certain parts of Java were left undocumented. The first and the most important for Java
programmers to remember is that not all implementations of Java can be guaranteed to support these features. What works in
Netscape may not work in Internet Explorer. Even more likely, what works in the appletviewer or HotJava may not work in
Netscape. This is sometimes a problem for applets, but it is a fully surmountable problem for applications.
The second common reason why a Java class is undocumented is that the source code is in rather poor shape. Many parts of Java are
held together by bubble gum and bailing wire. Java is full of quick fixes to unexpected problems pieced together by overworked
programmers with insufficient resources. The Java team was simply not prepared for the stunning success of Java, and Javasoft has
been desperately trying to catch up to itself. There simply hasn’t been enough time to whip the source code into shape while
simultaneously fixing bugs, writing documentation, negotiating licensing agreements, adding features, and planning for the future.
Some classes that were undocumented in Java 1.0, such as sun.net.MulticastSocket, became documented in Java 1.1, just as soon as
Javasoft had time to do it. This code is nonetheless useful now, and it is available to you even if it’s not documented.
The third and related reason why these features are undocumented is the fear that making them public hinders future modifications.
In many cases, JavaSoft may clean up the messy classes and quick fixes in the future and document them. Until then, however, they
would rather not get tied to their original ad hoc solutions that were never properly thought out. It is believed that Java is in a much
too early state of development to be locked into a half-baked API. This is almost certainly true. Java’s original event model was
completely revised between Java 1.0 and Java 1.1. However, because the 1.0 model was documented and in widespread use, Sun was
forced to continue to support it. Sun would rather limit the number of APIs locked into to the bare minimum until they feel more
confident that they’ve made the right decisions. This attitude places the focus on what may happen in the future rather than on what
is shipping today. At the speed at which the Web moves (One calendar month equals one Web year), programmers need solutions
today, not next month or next year.
According to Sun (http://java.sun.com/products/JDK/1.1/ compatibility.html [as of January 14, 1997]), when discussing the changes
from Java 1.0 to Java 1.1:
Some APIs in the sun.* packages have changed. These APIs are not intended to be used directly by developers.
They are there to support the java.* packages. Developers importing from sun.* do so entirely at their own risk.
The fourth and final reason that the topics of this book aren’t properly documented is the mistaken belief that Java programmers
simply don’t need to know. This confuses the issue of “need to know” with “need to use.” These are two different things. A deeper
understanding of how Java operates leads only to more efficient programs. You don’t absolutely have to know exactly how a Web
browser loads and instantiates an applet on a Web page to write applets, but if you do understand this, you’ll be able to write applets
that play more smoothly and load more quickly. You won’t actually call any of the undocumented methods and classes in your own
source code, but by knowing how they operate behind the scenes, you can map your use of the documented methods to work with
them instead of against them.
Finally, both what programmers need to know and what programmers need to use are closely tied to the sorts of applications that
programmers are building. What Javasoft has chosen to document so far assumes that programmers are building simple applets for
Netscape. In reality, this is a plurality but still a minority of the programs that people are actually writing. Many other things that
people are writing, especially development tools, need much more information than is required by a simple applet.
There is a certain amount of fear, uncertainty, and doubt about using undocumented Java classes. Is it safe? To borrow terminology
from AIDS educators, there’s no such thing as safe or unsafe Java. All Java programs are safe or less safe with varying degrees of
safety. Naturally you should always strive to have safer Java.
First of all, remember that Java has more built-in safeguards than almost any other language. A Java program is not going to work
today and crash your system tomorrow. Further, as long as users don’t change their Java environments, the programs that run today
should still run tomorrow.
Java will change. Methods that work today may not be present in future releases. Worse yet, they may be changed in future releases.
Early adopters have already been through several gut-wrenching transitions — worst of all the transition from alpha to beta — and
they survived. Time simply needs to be allotted for code to be rewritten.
There are many ways to guard against these problems. Java’s robust exception-handling mechanism provides an easy means to deal
with classes or methods that unexpectedly disappear between versions of Java. Code from the sun classes can be copied into your
own package or placed on your server so that it’s guaranteed to be available to an applet. Native methods can be backed up by Java-
only alternatives that are invoked if the native methods can’t be found. Versions can be checked to make sure an application is
running in a known environment.
However, that’s all in the future. Today, the programs that you write with the Sun classes and native code are safe. You should of
course try to use garden-variety, safe Java whenever possible, but don’t not write the next killer application simply because it
requires you to use a native method or to instantiate a sun class.
Some people have questioned whether the title Java SECRETS is truly appropriate for this book. Certainly a lot of the material here
is less secret than the internals of Microsoft Windows. Sun licenses the source code very freely (at least the code prior to Java 1.1),
and, as you’ll learn, the byte code is comparatively trivial to disassemble.
Source code may be a precise form of documentation, but it is hardly the easiest form to understand. Java source code has been
available since the earliest days, but it’s still left many people confused about exactly how to accomplish their goals. The content of
this book may not exactly be secret, but it certainly contains information that is not widely known. Frankly, given the quality of
information about these topics that is available, many of them might as well be secret. I expect this book to generate a phenomenal
level of interest among Java programmers, most of whom have only a vague idea that this sort of programming is possible in Java,
much less the knowledge to do it.
Summary
• A deeper understanding of how the Java runtime operates helps you write better programs, even if you don’t use that
knowledge explicitly in your source code.
• The sun packages are a group of classes included in Sun’s JDK and many other Java implementations that provide extra
capabilities to Java programs. They also support much of the Java infrastructure that applet programmers don’t always
need to think about.
• Native methods may not be pleasant, but they are often necessary. If you want to write classic, stand-alone applications
in Java that can compete with applications written in C and C++ in both speed and features, you will need to use native
methods.
• The techniques described in this book are powerful tools, but they are also more than a little dangerous. Using them
naïvely can and will produce unintended consequences. But with proper forethought and planning, you can prepare for
these problems and avoid them.
Chapter 2
Primitive Data Types
T he Java virtual machine defines eight primitive data types: five integer types, two floating-point types, and one boolean type.
The types are byte, short, int, long, float, double, char, and boolean. This chapter explores how these different primitive types are
stored in memory and used in calculations. You’ll learn how one can be converted to another and what can go wrong in this
conversion. You’ll also learn how to use the bit-level operators to reach down to the lowest level of the virtual machine and to
change what you find there.
Bytes in Memory
All data in Java (or any digital computer) must be represented as a particular sequence of bits in the computer’s memory. A bit is an
abstract quantity that can have exactly two values. These two values are commonly called 0 and 1. However, as you’ll see shortly,
these are not the same as the numbers zero and one.
At the very low level of electronic circuits, a transistor that is charged to a particular value — generally 5.0 or 3.3 volts relative to
ground — is said to be on and to have the value “one.” A transistor that is uncharged — at the value of 0.0 volts relative to ground —
is said to be off and have the value “zero.” However, when you consider matters at this low a level, the real world is analog, not
digital. It is possible for transistors to have voltages of 2.5 volts, 1.2 volts, -3.4 volts, or just about any other value you can imagine.
Most digital electronic circuits have some tolerance so that a transistor that’s on at 3.3 volts will still be on at 3.2 volts. Past that
tolerance, however, the transistor is said to be three-stating. This is a problem for the electrical engineers that design integrated
circuits, but it shouldn’t be a problem for a software engineer. If your computer starts three-stating when it isn’t supposed to, send it
back to the shop to be replaced.
Modern computers, including the Java virtual machine, organize bits into groups of eight called bytes. A group of eight bits is also
sometimes referred to as an octet. The single byte is normally the lowest level at which you can interact with a computer’s memory.
You always work with at least eight bits at a time. Bits are like hot dog buns. You can’t go to a grocery store and buy one hot dog
bun or 13 hot dog buns. Because hot dog buns come in packs of 8, you can get 8, 16, 24, or any other multiple of 8, but not any
number of buns that isn’t a multiple of 8. There is no keyword or operator in Java that enables you to read from or write to one bit of
memory at a time. You have to work with at least seven more bits adjacent to the bit you’re interested in at the same time, even if
you aren’t doing anything to those bits.
Note: This wasn’t always the case. Some early computers used 12-bit words. However, these computers have long since
become extinct.
Although you can buy as few as eight hot dog buns at a time, it’s sometimes cheaper to buy them by the case. The case size often
depends on where you buy them. At the corner convenience mart, 32 hot dog buns probably cost you four times as much as eight hot
dog buns. However, at Benny’s Super Discount Warehouse Store, buns may be cheaper by the gross. Similarly, different computers
pack different numbers of bytes into a word. Computers based on the Intel 8088 chip use 8-bit, 1-byte words. Computers based on
the 286 architecture, however, use 16-bit words and can therefore move data around at (very roughly) twice the speed of an 8088
computer at the same clock rate. Most modern CPUs use 32-bit words. The 32-bit processors include the 80386, 80486, Pentium,
Pentium Pro, Sparc, PowerPC 601, PowerPC 603, and PowerPC 604 CPUs. Some 64-bit processors are just starting to appear,
including Digital’s Alpha line, Sun’s UltraSparc chip, and the forthcoming HP/Intel Merced. All of these chips can still run old 8-bit
or 16-bit software, but they run faster and more efficiently with software that moves data around in words that match the native size
of the processor.
So which is Java? 8-bit? 16-bit? 32-bit? In fact, it’s really none of the above. Because Java uses only a virtual machine, it needs to be
able to run on any and all of the mentioned architectures without being tied to a particular word size. In one sense, you can argue that
the Java virtual machine is an 8-bit machine because each instruction is exactly one byte long. However, the native integer data type
for Java is 32-bit, so in that respect, Java is a 32-bit computer. The interpreter or JIT will likely convert the Java instructions and data
into whichever format is appropriate for the machine on which it’s running.
Variables, values, and identifiers are closely related to each other. In common use, the three words are used interchangeably.
However, each word does have a slightly different meaning, and when you discuss computers at the CPU or virtual machine level,
these differences become important.
int j = 2;
The letter “j” is an identifier. It identifies a variable in Java source code. The identifier, however, does not appear in the compiled
byte code. It is a mnemonic device to make programmers’ lives easier. The number 2 is the value of the variable. To be more precise,
the bit pattern 00000000000-000000000000000000010 is the value of the variable. The four bytes of memory where this pattern is
stored are the variable.
A variable is a particular group of bytes in the computer’s memory. The value of a variable is the bit pattern stored in those bytes
that make up the variable. How the bit pattern is interpreted depends on the type of the variable. The rest of this chapter discusses the
interpretation of the bit patterns that make up different primitive data types.
You can change the value of a variable by adjusting the bits that live in those bytes. This does not make it a new variable.
Conversely, two different variables can have the same value.
An identifier is a name for a particular group of bytes in memory. Some programming languages allow a single variable to have
more than one name. However, Java does not. In a Java program, an identifier always points to a particular area of memory. Once an
identifier has been created, there is no way to change where it points.
Note: This may sound a little strange to experienced Java programmers. In particular, you may think that this is true for
primitive data types like int but not for object types like String. In fact, this is true for all Java data types. You’ll have to
wait till the next chapter to see why.
The bits in memory aren’t just random voltages. They have meanings, and the meanings depend on the context. In one context, the
bit sequence 0000000000100001 means the letter “A.” In another context, it means the number 65. Let’s explore how you get the
number 65 out of the bits 0000000000100001.
When you write a number like 1406 in decimal notation, what you really mean is one thousand, four hundreds, no tens, and six ones.
This may seem trivially obvious to you. After all, you’ve had this system drilled into you since early childhood. However, the place-
value number system in which there are exactly ten digits and numbers larger than nine are represented by moving the digits further
to the left is far from obvious. It took humanity most of its existence on this planet to develop this form of counting, and it didn’t
become widespread, even in Eurasia, until well into the second millennium. It’s even less obvious that the digits on the left represent
bigger numbers than the digits on the right. You could just as easily write the number as 6041 with the understanding that the first
place is the ones place, the second place the tens place, the third place the hundreds, and so on.
Note: Classical Hebrew writes numbers from right to left. However, it doesn’t use a place-value system.
Binary notation
The number 0000000000100001 that you saw in the preceding section is written in a place-value system based on powers of two
called binary notation. Each place is a power of two, not of ten, and there are only two digits — 0 and 1. Moving from right to left,
therefore, we have one one, zero twos, zero fours, zero eights, zero sixteens, zero thirty-twos, and one sixty-four. Therefore,
0000000000100001 is equal to 64 + 1, or 65, in decimal notation.
There are extra zeroes on the left side because Java uses bits only in groups of eight at a time, although the individual bits do have
meaning. Furthermore, as you’ll see below, characters like A are always 16 bits wide. You could use 0100001 to represent the value
65, but unlike 0000000010-0001, it would not also mean the letter A.
Java has several methods to convert between binary and decimal notation. The Integer and Long classes each have a static
toBinaryString() method which converts ints and longs respectively to binary strings of ones and zeroes. For example, to print the int
value 65 as a binary string, you could write
System.out.println(Integer.toBinaryString(65));
System.out.println(Long.toBinaryString(5000000000L));
Secret: The Byte and Short classes do not have toBinaryString() methods, but bytes and shorts can be converted using the
Integer.toBinaryString() method.
Given a binary string of ones and zeroes, the Byte, Short, Integer, and Long classes each have static valueOf() and parse methods
that convert binary strings into integers of the specified width.
The Byte.parseByte(String s), Short.parseShort(String s), Integer. parseInt(String s), and Long.parseLong(String s) methods convert
a string like “28” into a byte, short, int, or long value respectively. These methods presume that the string is written in base 10.
However, you can change the base that’s used to make the conversion by passing an additional int containing the base to the method,
like this:
To convert the binary string 00000000100001 into byte, short, int, and long values of 65, you would write
If the string does not have the form appropriate for the base you specify in the second argument (for example, if you try to convert
the string “97” in base 2), then a NumberFormatException will be thrown.
The static valueOf() methods in the Byte, Short, Integer, and Long classes are very similar except that they return objects of the type-
wrapper classes rather than primitive data types. For example:
Hexadecimal notation
One reason why humans use a system with ten digits instead of one with two digits is that it’s quite hard to read numbers like
00000000100001. Converting between binary and decimal notation requires substantial arithmetic, but converting between binary
and hexadecimal notation can be done with the much faster table-lookup approach.
Undoubtedly, the main reason humans use decimal notation instead of binary notation is that we have ten fingers on our hands.
Indeed that’s why the word digit is used to refer to both the characters between 0 and 9 and our fingers and toes. Humans use both
things to count with. Doubtless if we ever encounter a sentient alien race, their number system will be based on the number of
fleshy protuberances they count with.
However, not all human societies have used decimal notation. The Mayans used a system with twenty digits, and the Babylonians
had an unbelievable 60 different digits, although only two symbols were used to form each of the 60 digits in a predictable way.
Hexadecimal notation uses 16 digits and a place-value system based on powers of 16. As well as the customary 0 through 9, there are
also A (10), B (11), C (12), D (13), E (14), and F (15). These extra digits are normally written using uppercase letters, but the
lowercase a, b, c, d, e, and f are sometimes used instead. Thus in hexadecimal notation, 65 is written as 41; that is, four times 16 plus
one times 1. The hexadecimal number E3 is decimal 227; 14 times 16 plus 3.
There are always exactly four bits in one hexadecimal digit, so binary 0000 is always hex 0, binary 1000 is always hex 8, and binary
1111 is always hex F. This is in contrast to decimal notation, where the digits 0 through 7 can be encoded in three bits but the digits 8
and 9 require four bits. If you do use four bits for each decimal digit, you also have six 4-bit patterns that don’t correspond to a digit.
Table 2-1 lists the binary equivalents of the 16 hexadecimal digits. You can convert a number from binary to hexadecimal and vice
versa by simple substitution of bit patterns according to this table. Conversion to decimal requires quite a bit more effort.
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 A
1011 B
1100 C
1101 D
1110 E
1111 F
There’s one more advantage to a hexadecimal number system: Two hexadecimal digits equal one byte. Any single-byte value can be
written as exactly two hexadecimal digits, and any pair of hexadecimal digits is exactly one byte. Therefore, hexadecimal digits are
often used to represent the state of a computer’s memory in a compact and relatively easy-to-read fashion. Disk editors can display
the contents of hard drives as sequences of hexadecimal numbers. And, as you soon learn, you can read and write Java .class byte
code files by treating them as sequences of hexadecimal numbers.
Hexadecimal digits are so useful in computer programming that Java even lets you write integer literals as hexadecimal digits. To
use a hexadecimal literal instead of a decimal literal, prefix it with 0x or 0X. Java does not care whether you use small letters or
capital letters in your hexadecimal literals. For example, the following five lines of code each say the same thing:
int n = 227;
int n = 0xE3;
int n = 0xe3;
int n = 0Xe3;
int n = 0XE3;
When using hexadecimal numbers, most programmers choose the form 0xE3 with a small x and capital hex digits. This is slightly
easier to read and understand than the other three forms.
Java has several methods to convert between decimal and hexadecimal notation. The Integer and Long classes each have a static
toHexString() method that converts ints and longs respectively to hexadecimal strings. For example, to print the int value 1024 as a
hexadecimal string, you could write
System.out.println(Integer.toHexString(1024));
System.out.println(Long.toHexString(5000000000L));
Secret: The Byte and Short classes do not have toHexString() methods, but bytes and shorts can be converted using the
Integer.toHexString() method.
You can convert a hexadecimal string to a numeric value using the parse and valueOf() methods described in the last section. Just
pass 16 as the base argument instead of 2. For example:
Octal notation
Java also allows the use of a base-eight notation with eight digits called octal notation. An octal digit can be represented in three bits.
For example, 011 is octal 3. Table 2-2 lists all the octal digits and their equivalent binary patterns. Notice that this is the same as the
first eight rows of Table 2-1 with the initial zero removed from each bit pattern.
000 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7
Although the words octal and base eight sound like they should be closely related to the eight bits in a byte, in reality they’re not.
You cannot write a byte value as a certain number of octal digits because the three bits in an octal digit do not evenly divide the eight
bits in a byte. Therefore, octal numbers aren’t nearly as useful in practice as hexadecimal numbers. Their presence in Java is a
holdover from their presence in C. Octal numbers were included in C because they are quite useful on machines with 12-bit words.
The three bits in an octal number divide evenly into 12 bits, and computers with 12-bit words were still being used when C was
created.
To use an octal literal in Java code, just prefix it with a leading 0. For example, to set n to decimal 227, you could write
int n = 0343;
Note: I can think of no reason why you might want to do this. If you do this, please write and tell me why.
Java has several methods to convert between decimal and octal notation. The Integer and Long classes each have a static
toOctalString() method which converts ints and longs respectively to octal strings. For example, to print the int value 1024 as an
octal string, you could write
System.out.println(Integer.toOctalString(1024));
Longs are converted similarly:
System.out.println(Long.toOctalString(5000000000L));
Note: The Byte and Short classes do not have toOctalString() methods, but bytes and shorts can be converted using the
Integer.toOctalString() method.
You can convert an octal string to a numeric value using the parse and valueOf() methods described in the last section. Just pass 8 as
the base argument instead. For example:
Integers
An integer is a mathematical concept that describes a whole number. One, two, zero, 72, -1,324, and 768,542,188,963,243,888 are all
examples of integers. There’s no limit to the size of an integer. An integer can be as large or as small as it needs to be, although it
must always be a whole number like seven and never a fraction like seven and a half.
Java’s integer data types map pretty closely to the mathematical ideal, with the single exception that they’re all of finite magnitude.
The four integer types — byte, short, int, and long — differ in the size of the numbers they can hold, but they all hold only a finite
number of different integers. Most of the time this is enough.
ints
In Java, an int is composed of four bytes of memory — that is, 32 bits. Written in binary notation, an integer looks like
01001101000000011100101010001101
8D01BA8D
Each of the rightmost 31 places is a place value. The rightmost place is the one’s place, the second from the right is the two’s place,
the third from the right is the four’s, the fourth from the right is the eight’s, and so on, up to the 31st place from the left, which is the
1,073,741,824’s place.
The largest possible int in Java has all bits set to one except the leftmost bit. In other words, it is
01111111111111111111111111111111, or, in decimal, 2,147,483,647.
You’re probably thinking that we could set the leftmost bit to one, and then have 11111111111111111111111111111111 as the
largest number, but the leftmost bit in an int isn’t used for place value. It’s used to indicate the sign of the number and is called the
sign bit. If the leftmost bit is one, then the int is a negative number. Therefore, 11111111111111111111111111111111 is not
4,294,967,295 but rather -1.
Java, like most modern computers, uses two’s complement binary numbers. In a two’s complement scheme, to reverse the sign of a
number, you first take its complement — that is, convert all the ones to zeroes and all the zeroes to ones — and then add one. For
example, to convert the byte value 0100001 (decimal 65) to -65, you would follow these steps:
65: 0100001
65 complement: 1011110
Add 1: +0000001
-65: 1011111
Here I’ve worked with 8-bit numbers instead of the full 32-bit ints used by Java. The principle is the same regardless of the number
of bits in the number.
To change a negative number into a positive number, do exactly the same thing. For example:
-65: 1011111
-65 complement: 0100000
Add 1: +0000001
65: 0100001
One of the advantages of two’s complement numbers is that the procedure reverses itself. You don’t need separate circuits to convert
a negative number to a positive one.
Computer integers differ from the mathematical ideal in that they have maximum and minimum sizes. The largest positive integer
has a zero bit on the left side and all remaining bits set to one — that is, 0111111111111111-1111111111111111, or 2,147,483,647
in decimal. If you try to add one to this number as shown here, the one carries all the way over into the leftmost digit. In other words,
you get 10000000000000000000000000000000, which is the smallest negative int in Java, decimal -2,147,483,648.
01111111111111111111111111111111
+ 00000000000000000000000000000001
10000000000000000000000000000000
Further addition will make the negative number count back up to zero and then into the positive numbers. In other words, if you
count high enough, eventually you wrap around to very small numbers. The next int after 2,147,483,647 isn’t 2,147,483,648. It’s -
2,147,483,648. If you need to count higher than 2,147,483,647 or lower than -2,147,483,648, then you need to use a long or a
floating-point number, as I discuss in the next sections. These numbers have maximums and minimums of their own; they’re just
larger ones.
So far we’ve worked with 32-bit ints. Java provides three other integer data types: byte, short, and long. These have different bit-
widths, and they’re not as easy to use as literals in Java source code, but their analysis is exactly the same as that of ints.
One’s Complement
Some early computers used one’s complement arithmetic instead. In one’s complement, you invert all the bits to change the sign
of a number, as you do in two’s complement, but you don’t add 1. Thus, since 65 is 0100001 and -65 is 1011110. This seems
simpler. However, you encounter a problem with zero. Zero itself is 00000000. Negative zero is 11111111. But negative zero is
supposed to be the same as positive zero. Adding one to 11111111, as you do in two’s complement, flips all the bits back to 0 as
the one carries across to the left and disappears. In two’s complement notation, therefore, 0 and -0 have the same bit pattern. This
advantage has led to the triumph of two’s complement computers in the marketplace. One’s complement computers died off even
before 12-bit word machines did.
A byte is eight bits wide. The largest byte is 01111111, or 127 in decimal. The smallest byte is 10000000, or -128 in decimal. Bytes
are the lowest common denominator for data interchange between different computers, and Java uses them extensively in input and
output. However, it does not use byte values in arithmetic calculations or as literals. The Java compiler won’t even let you write code
like the following:
byte b3 = b1 + b2;
If you try this, where b1 and b2 are byte variables, you’ll get an error message that says Error: Incompatible type for
=. Explicit cast needed to convert int to byte. This is because the Java compiler converts bytes to ints before
doing the calculation. It does not add b1 and b2 as bytes, but rather as ints. The result it produces and tries to assign to b3 is also an
int.
Shorts are 16 bits wide. The largest short is 0111111111111111, or 32,767 in decimal. The smallest short is 1000000000000000, or -
32,768 in decimal. There is no way to use a short as a literal or in arithmetic. As with bytes, if you write code like
you’ll get an error message that says: Error: Incompatible type for =. Explicit cast needed to convert
int to short. The Java compiler converts all shorts to ints before doing the calculation. The only time shorts are actually used
in Java is when you’re reading or writing data that is interchanged with programs written in other languages on platforms that use 16-
bit integers. For example, some old 680X0 Macintosh C compilers use 16-bit integers as the native int format. Shorts are also used
when very many of them need to be stored and space is at a premium (either in memory or on disk).
The final Java integer data type is the long. A long is 64 bits wide and can represent integers between -9,223,372,036,854,775,808
and 9,223, 372,036,854,775,807. Unlike shorts and bytes, longs are directly used in Java literals and arithmetic. To indicate that a
number is a long, just suffix it with the letter L — for example, 2147483856L or -76L. Like other integers, longs can be written as
hexadecimal and octal literals — for example, 0xCAFEBABEL or 0714L.
Note: You can use either a small l or a capital L to indicate a long literal. However, a capital L is strongly preferred
because the lowercase l is easily confused with the numeral 1 in most typefaces.
Floating-Point Numbers
Integers aren’t the only kind of number you need. Java also provides support for rational numbers — numbers with a decimal point
like 106.53 or -78.0987. For reasons you’ll learn shortly, these are called floating-point numbers, and Java has two primitive data
types for them: the float and the double.
Floating-point literals can be made quite large or quite small by writing them in exponential notation — for example, 1.0E89 or -
0.7E-32. The first is 1.0 × 1089, in other words 1 followed by 89 zeroes. The second is -0.7 × 10-32 or -
0.00000000000000000000000000000007.
A floating-point number can be split into three parts: the sign, the mantissa, and the exponent. The sign tells you whether the number
is positive or negative. The mantissa tells you how precise the number is. Generally, the more digits a number has, the more precise
it is. Finally the exponent tells you how large or small the number is. In the number 0.7E-32, the sign is -, the mantissa is 7, and the
exponent is -32. In 1.0E89, the sign is +, the mantissa is 1, and the exponent is 89.
Although Java does not put any particular limits on the number of digits a float or double literal can have before the decimal point, it
is customary to place exactly one non-zero digit before the decimal point and all the rest after it and adjust the exponent to
compensate. Thus, instead of writing 15.4 × 1089, you would write 1.54 × 1090. This is called scientific notation. An alternative
custom called exponential notation places the first non-zero digit immediately following the decimal point. In exponential notation,
15.4 × 1089 becomes 0.154 × 1091.
The advantage to such a custom is that you no longer actually have to write the decimal point. If you know that the decimal point is
always going to be immediately after the first non-zero digit, as it is in scientific notation, then why bother writing it down? Of
course, not writing it makes it harder for human beings to read and understand the number, so the decimal point is required in Java
source code. Computers can do quite well without an explicit decimal point as long as the byte code sticks to a form of scientific
notation.
Once we’ve agreed that floating-point numbers will always be written in scientific notation, the mantissa, exponent, and sign of a
floating-point number can all be written as integers. Just like the sign bit in integer data types, 1 represents a positive number and 0
represents a negative number. For example, 15.4 has sign 1, mantissa 154, and exponent 1. The number -0.7 × 10-32 has sign 0,
mantissa 7, and exponent -32.
To represent a floating-point number in a computer, you must convert each of these values into bits and binary notation. Converting
a number with a decimal point into binary notation is only slightly harder than converting a number without a decimal point. When
you write the number 10.5, you mean one ten, no ones, and five tenths. In binary notation you use a binary point rather than a
decimal point (though they look exactly the same on the printed page.) Thus, a real number in binary notation looks like 1010.1. This
means a number with one eight, no fours, one two, no ones, and one half. In other words, this is 8 + 2 + 0.5 = 10.5 in decimal
notation.
Binary floating-point numbers in Java are written in normalized form. This means that the leftmost one is shifted to the immediate
right of the binary point. An exponent is then added as a power of two. Thus 1010.1 becomes 0.10101 × 10100 (where 10100 is 24 in
decimal). The sign is 1, the mantissa is 10101, and the exponent is 100.
But wait! It gets better. When you’re using binary notation, the only non-zero digit is 1. The first non-zero digit after the binary point
must be 1 because it can’t be anything else. Therefore, you don’t need to write it down either. You get an extra bit of precision,
essentially for free. To store the mantissa 10101, you only need to write the bits 0101.
The next step is to determine how these numbers will be stuffed into bytes. Java allots four bytes for each float and eight bytes for
each double. The first bit of each float is used for the sign bit. A 1 bit is negative and a 0 bit is positive, exactly as with integers.
The next eight bits are used for the exponent. These eight bits are treated as an unsigned integer between 0 and 255. The numbers 0
and 255 have special meanings that I discuss shortly. Otherwise, the exponent is biased by subtracting 127 from it. Therefore, float
exponents have values between -126 (1 - 127) and +127 (254 - 127). Here’s what this arrangement looks like:
01111111111111111111111111111111
00000000000000000000000000000001
1000000000000000000000000000000
The final 23 bits are used for the mantissa. The mantissa is given as a fractional number between 1 and 2. As discussed earlier in this
chapter, the first bit is assumed to be one, so the mantissa effectively has 24 bits of precision. Extra zeroes are appended if necessary.
This doesn’t change the number, though, because 1.0101000000000000000000 is exactly the same as 1.0101. In other words, you
can always add extra zeroes at the end of the mantissa to fill space. Figure 2-1 shows the bits in a float.
Note: The description that I’ve adopted here is the one used by the IEEE 754 specification. In this description, the
mantissa is a normalized, binary, rational number — that is, its value is a fraction between 1 and 2. The Java Language
Specification uses an alternate but equivalent description in which the mantissa is interpreted as an integer between 223
and 224-1. In this description, the bias used on the exponent is 150 — that is 127 + 23. A little thought should convince you
that these descriptions are equivalent.
Finite precision
It’s important to understand that not all floating-point numbers can be exactly represented in a finite number of bits. For example,
whereas one half is exactly 0.1 (binary) or 0.5 (decimal), one third in binary is 0.0101010101 . . . where the pattern repeats
indefinitely. One third also repeats in decimal notation where it’s 0.33333333 . . . . Whether or not a number repeats or terminates
depends on the base of the number system. One fifth is exactly 0.2 in decimal, but is 0.0011001100110011 . . . in binary. Some
numbers, most famously [Pi], neither terminate nor repeat. Because computer arithmetic must truncate these infinite mantissas to just
24 bits, computer arithmetic on floats is often imprecise. The best Java can do with a number like [Pi] is approximate with an
accuracy of 24 bits.
Doubles
If a float is not precise enough or large enough, you can use a double instead. A double has eight bytes, of which 1 bit is used for the
sign, 11 bits for the exponent, and 53 bits for the mantissa. If you’re sharp, you’ll notice that this adds up to 65 bits. Don’t forget that
the first bit of the mantissa is always 1, so you don’t need to store that bit. The exponent is biased by subtracting 1023.
Special values
Java’s floating-point numbers aren’t limited to the rational numbers you learned in high school. There are several special numbers
that, while not true numbers in the traditional sense of the word, are produced by some calculations. If the non-biased exponent is
255, then the number takes on one of several special meanings.
Inf
Java has two special floating-point values to represent positive and negative infinity. There’s no literal for these infinities, but the
public final static float values java.lang.Float.POSITIVE_INFINITY and java.lang.Float. NEGATIVE_INFINITY allow you to use
them in source code.
More commonly you’ll bump across these values unexpectedly when a calculation goes in a direction that you didn’t anticipate.
Positive infinity is produced when a positive float or a double is divided by zero. Dividing a negative float or double by zero gives
negative infinity. For example:
double x = 1.0/0.0;
There’s little reason to deliberately create a float or double that’s infinite. However, it is a rather common thing to create one
accidentally in more complicated programs where all possible divisors aren’t determined until runtime. The Inf value lets your
programs continue without crashing or throwing an exception.
You can get the value Inf only in a floating-point calculation. If you try to divide an integer by integer zero, an ArithmeticException
is thrown instead. For example:
int i = 1/0;
In a comparison test with <, <=, >, or >=, -Inf is smaller than any other number and Inf is larger than any other number. Each is equal
only to itself.
The bit patterns for positive infinity and negative infinity are formed by the appropriate sign bit (1 for negative, 0 for positive), an
unbiased exponent of 255 (11111111), and a mantissa of zero. Thus, positive infinity is 01111111100000000000000000000000, or
in hexadecimal, 7F800000. Negative infinity is 11111111100000000000000000000000, or in hexadecimal, FF800000.
Double positive and negative infinity are formed in the same way. Choose the appropriate sign, fill the exponent with one bits, and
set the mantissa to zero. Thus, positive double infinity is 7FF0000000000000 and negative double infinity is FFF0000000000000.
NaN
NaN is an acronym for “Not a Number.” A floating-point calculation returns NaN if it divides zero by zero. For example:
double z = 0.0/0.0;
You can also get NaN values in certain other undefined arithmetic operations, such as taking the square root of a negative number or
raising zero to the zeroth power.
There is no literal that lets you type NaN into Java source code, but you can get the same effect with the public, final, static float
constant java.lang.Float.NaN.
More commonly, NaN will pop up unexpectedly. For example, the following code fragment divides 0.0 by 0.0 when x is equal to 5.0:
double y = 10.0;
for (double x = 0.0; x <= y; x+=1.0, y -= 1.0) {
double z = x - 5.0;
double result = (x - y)/z;
System.out.println(x + " " + y + " " + z + " " + result);
}
NaN is unordered, so the result will always be false if you compare it to other numbers with <, <=, >, >=, or ==. The only
comparison that can return true is !=, which always returns true if one or both of the operands is NaN. In other words, NaN is never
equal to any number (including itself), never greater than any number, and never less than any number.
Although division by zero does not crash your program like it does in some programming languages, the unexpected appearance of
NaNs or Infs in program output generally indicates a bug that needs to be stomped. Real world quantities shouldn’t be infinite or
“Not a Number.” If you see NaNs or Infs, it may be an indication that a small factor you left out of your analysis, friction for
example, is becoming important in a special case because everything else is canceling out.
NaN is represented by any float or double bit pattern in which the exponent is all ones and the mantissa is non-zero. (If the mantissa
is zero, then the number is either positive or negative infinity.) The sign bit is ignored because NaN is not signed. Thus, all floats
from 7F800001 to 7FFFFFFF and from FF800001 to FFFFFFFF correspond to NaN. All doubles from 7FF0000000000001 to
7FFFFFFFFFFFFFFF and from FFF0000000000001 to FFFFFFFFFFFFFFFF also correspond to NaN.
The smallest value that you can represent in Java is java.lang.Double. MIN_VALUE, 4.94065645841246544e-324. Numbers with
absolute values smaller than this are set to zero. However, the sign of the number can be retained if the number is in fact non-zero.
The normal 0.0 you type in source code is positive zero. You get negative zero when you multiply a negative number by zero. For
example:
In direct comparisons, negative zero and positive zero appear to be equal. However, some other operations will produce different
results depending on whether positive zero or negative zero is used. For example, 1.0 divided by positive zero is positive infinity, but
1.0 divided by negative zero is negative infinity.
The zero literal you type into source code with 0.0 or 0.0F is always positive zero. You can get negative zero only if it shows up in a
calculation.
Positive zero is, as you would expect, the float or double value whose bits are all zero. In other words, float positive zero is
0000000000000000-0000000000000000, or 00000000 in hexadecimal. Negative zero is the same, except that the sign bit is one.
Thus, float negative zero is 10000000-0000000000000000000000000, or 80000000 in hexadecimal. Double positive zero is
0000000000000000 in hexadecimal, and double negative zero is 8000000000000000.
Numbers whose unbiased exponent is zero but whose mantissa is not zero are denormalized. Denormalized numbers do not have an
implied first bit with value one. All of the bits that a denormalized number has are present in the mantissa. The mantissa is presumed
to be multiplied by 2-127 In other words, it acts like it has a biased exponent of -127, or an unbiased exponent of zero. In fact, this is
exactly what it does have, so the only real difference between normalized and denormalized floating point numbers is the implied
first bit.
Unlike Inf, NaN, and positive and negative zero, all of which can appear in one form or another in Java source code or output,
denormalized numbers don’t look any different from regular floating point numbers. However, being able to recognize and decode
them will become important when you learn how to disassemble Java byte code in Chapters 4 and 5.
CHAR
The char data type in Java is considered to be a number, but it’s a funny one. Most obviously, when you try to print a char, you don’t
get a number. Rather you get a character like “a” or “#”. Secondly, char literals don’t look like numbers in source code. You
normally enter a char like this:
char c = `r';
You can, however, use integer literals to assign values to char variables. The following statement does exactly the same thing as the
previous one:
char c = 114;
You don’t often see Java source code that initializes chars with integer literals, because most programmers don’t walk around with
the entire ASCII chart in their head. The meaning of the first statement is much more obvious than the meaning of the second, but
they produce identical byte code.
Chars are two bytes wide-they take up the same space as a short. However, chars are not shorts. Shorts are signed and chars are
unsigned. The first bit in a char is the 32,768 place, not a sign bit. Thus, while 1000000000000001 interpreted as a short is -32,768,
1000000000000001 interpreted as a char is 32,769. Chars range from 0 to 65,535.
The Java compiler has to work a little magic to handle this. The line
char c = 114;
char d = 45000;
Both 114 and 45000 are within the range of a char. However, the following two lines produce compile-time error messages, telling
you an explicit cast is needed to convert an int to a char:
char e = -123;
char f = 65536;
Java characters are understood to be part of the Unicode character set. The Unicode character set has, at the time of this writing,
38,885 characters, each two bytes wide. Unicode scripts include alphabets used in Europe, Africa, the Middle East, India, and many
other parts of Asia, as well as the unified Han set of East Asian ideographs and the complete ideographs for Korean Hangul. Some
scripts are not yet supported or are only partially supported, primarily because these scripts are not yet well understood.
Unsupported scripts include Braille, Cherokee, Cree, Ethiopic, Khmer (a.k.a. Cambodian), Maldivian (a.k.a. Dihevi), Mongolian,
Moso (a.k.a. Naxi), Pahawh Hmong, Rong (a.k.a. Lepcha), Sinhalese, Tagalog, Tai Lu, Tai Mau, Tifinagh, Yi (a.k.a. Lolo), and
Yoruba. Cherokee, Ethiopic, Braille, and possibly Khmer are likely to be added in the near future. Some of these languages can be
written with other scripts that Unicode does support. For example, Mongolian is commonly written using the Cyrillic alphabet, and
Hmong can be written in ASCII.
Furthermore, Unicode does not support many archaic alphabets, including Ahom, Akkadian Cuneiform, Aramaic, Babylonian
Cuneiform, Balinese, Balti, Batak, Brahmi, Buginese, Chola, Cypro-Minoan, Egyptian hieroglyphics, Etruscan, Glagolitic, Hittite,
Javanese (a particularly galling omission), Kaithi, Kawi, Khamti, Kharoshthi, Kirat (Limbu), Lahnda, Linear B, Mandaic, Mangyan,
Manipuri (Meithei), Meroitic (Kush), Modi, Numidian, Ogham, Pahlavi (Avestan), Phags-pa, Pyu, Old Persian Cuneiform,
Phoenician, Northern Runic, Satavahana, Siddham, South Arabian, Sumerian Cuneiform, Syriac, Tagbanuwa, Tircul, and Ugaritic
Cuneiform. Runic and Ogham are likely to be added in the near future. Some of the rest of these languages, such as Linear B, are still
areas of active research among linguists. Of the remainder, few (if any) are likely to be added to Unicode in the foreseeable future,
even those that are fairly well understood.
Theoretically, Unicode can be expanded to cover up to 65,536 different characters. This is not quite enough to handle every character
from all the world’s alphabets, primarily because of the large number of characters in the pictographic alphabets used for Chinese,
Japanese, and historical Vietnamese. The Chinese alphabet alone has more than 80,000 different characters. However, by combining
similar characters in these four alphabets so that some chars represent different words in different languages, all of the alphabets and
the most commonly used pictographs can be squeezed into two bytes.
ASCII
Unicode is based on two character sets that predate it: ASCII and ISO Latin-1. ASCII is a 7-bit character set with 128 different
characters. ASCII was designed for communication in United States English. It therefore contains the lowercase letters a-z, the
capital letters A-Z, the digits 0-9, various punctuation marks, and a number of non-printing control characters, many of which are
closely related to the types of terminals and printers that were in use when ASCII was invented. The characters in ASCII are
numbered from 0 to 127. Character 0 is the non-printing null character. Character 127 is the delete character. Characters 48 through
57 are the digits 0 through 9. Characters 65 through 90 are the capital letters A through Z. Characters 97 through 122 are the
lowercase letters a through z. The remaining ASCII characters are various punctuation marks and non-printing characters. Table 2-3
is a complete list.
0 null 32 space 64 @ 96 `
1 soh 33 ! 65 A 97 a
2 stx 34 " 66 B 98 b
3 etx 35 # 67 C 99 c
4 eot 36 $ 68 D 100 d
5 enq 37 % 69 E 101 e
6 ack 38 & 70 F 102 f
7 bell 39 ' 71 G 103 g
8 backspace 40 ( 72 H 104 h
9 tab (\t) 41 ) 73 I 105 i
10 linefeed (\n) 42 * 74 J 106 j
11 vertical tab 43 + 75 K 107 k
12 formfeed (\f) 44 , 76 L 108 l
carriage return,
13 45 - 77 M 109 m
(\r)
14 so 46 . 78 N 110 n
15 si 47 / 79 O 111 o
16 dle 48 0 80 P 112 p
17 dc1 49 1 81 Q 113 q
18 dc2 50 2 82 R 114 r
19 dc3 51 3 83 S 115 s
20 dc4 52 4 84 T 116 t
21 nak 53 5 85 U 117 u
22 syn 54 6 86 V 118 v
23 etb 55 7 87 W 119 w
24 can 56 8 88 X 120 x
25 em 57 9 89 Y 121 y
26 sub 58 : 90 Z 122 z
27 escape 59 ; 91 [ 123 {
28 is4 60 < 92 \ 124 |
29 is3 61 = 93 ] 125 }
30 is2 62 > 94 ^ 126 ~
31 is1 63 ? 95 _ 127 delete
ISO Latin-1
As I said, ASCII is designed to handle U.S. English. It can do a reasonable approximation of other dialects of English, but it begins
to have problems with many other European languages, like French and German. There are no cedillas, umlauts, or any of the other
characters not used in English, but present in these languages.
The first bit of each ASCII character is 0. You can define another 128 characters by using the bytes whose first bit is one. Indeed,
this is the scheme used in most modern computers. The characters with numeric values between 128 and 255 are used to encode the
additional characters needed by most languages that are written in some approximation of the Latin alphabet. There are at least two
common ways ASCII is extended into the upper 128 characters. The one around which Unicode and Java are built is the ISO 8859-1
Latin-1 character set, often just referred to as ISO Latin-1. Table 2-4 lists the upper 128 characters of the ISO Latin-1 character set.
The lower 128 characters are exactly the same as they are for ASCII.
Unicode
Just as ISO Latin-1 extends ASCII by adding an extra high-order bit, so too does Unicode extend ISO Latin-1 by adding an extra
high-order byte. If the high-order byte is zero (00000000), then the Unicode character is identical to the ISO Latin-1 character in the
low-order byte. You can do an approximate conversion from Unicode to ISO Latin-1 by chopping off all the high-order bytes. This
works as long as all the text is composed only of ISO Latin-1 characters. Most of the time, especially when you’re working in
English, this is a reasonable assumption. Many of Java’s classes that output text make this assumption, most notably PrintStream,
which includes System.out.
Note: I’d love to show you a table of all the extra characters in Unicode, but it would be so lengthy that this book would
be mostly that table and not much else. If you need to know more about the specific encodings of the different characters
in Unicode, you should check out The Unicode Standard, Second edition, ISBN 0-201-48345-9, from Addison-Wesley.
This 950-page book includes the complete Unicode 2.0 specification. Errata for this volume are on the Web at http://www.
unicode.org/.
Mac Roman
Remember that I said there were two ways to encode these extra characters in the upper 128 bytes? The Macintosh uses a
completely different character-encoding scheme called Mac Roman. It has most of the same glyphs as the ISO Latin-1 character
set, but different glyphs are mapped to different numbers. If Java programs try to print the upper 128 characters on a Macintosh,
they come out in the Mac Roman character set, not the ISO Latin-1 character set like they are supposed to.
This is a royal pain for more than just Java programs because it makes file translation between platforms excessively difficult. In
fact, Java 1.1 provides one of the few class libraries that can translate between the Mac Roman and ISO Latin-1 character sets.
This is especially painful to authors trying to write about ISO Latin-1 on a Macintosh.
When the Macintosh was created in the early 1980s, it was one of the very few computers that could handle non-ASCII text. ISO
Latin-1 was not yet established. Therefore, Apple had to invent their own scheme for encoding the extra characters. Regrettably,
backward-compatibility means that Macs will never get in sync with the rest of the world. That’s one of the disadvantages of
pioneering new technology.
To make matters worse, it’s happening again. Apple developed their 2-byte WorldScript technology before Unicode was ready.
Everyone who came after Apple standardized on Unicode. This means that we’re probably stuck with ASCII as the lowest
common denominator for text data for the foreseeable future.
Because very few text editors are available that allow you to write in Unicode, Java source code files are written in ISO Latin-1.
Furthermore, the Java compiler expects to see source code written in ISO Latin-1. If you actually have a text editor that works in
Unicode and try to write Java files with it, the compiler will get hopelessly confused when it tries to compile your files.
In fact, Java can be written perfectly well with only ASCII. All Java keywords, operators, and literals, as well as all method, class,
and field names in the java packages, can be written in pure ASCII. Because ISO Latin-1 makes your source code difficult to move
between Macs and other platforms, you should probably restrict yourself to ASCII in your programs.
You can use Unicode characters in Java string and char literals as well as in identifiers. To embed a non-ASCII character in a string,
prefix the hexadecimal number for the character with \u. For example, the division sign is Unicode character 247. Therefore, you can
make it part of the string by writing \u00F7. The Greek letter [pi] is Unicode character 12,480 or hexadecimal \u03C0. Thus,
All Unicode characters can be encoded in this fashion, even those you could type literally. For example, the small letter t can also be
written as \u0074. The backslash itself can be written as \u005C. Writing code this way is a very bad idea unless you’re deliberately
trying to make it obscure.
When a Java compiler reads Java source code, it first converts all such \u escapes to the actual characters, taking into account double
backslash escapes as well. This pre-processing happens before anything else. For example, consider this statement:
The double backslash is interpreted as a literal backslash, not as the start of an escape sequence. Thus you get “This is not a \u0074”
instead of “This is not a \t.” To get the second effect, you would have to write
Unicode escape translation is not cumulative. “\u005Cu0074” is translated to the six characters “\u0074” rather than the single
character “t.”
As if Unicode input to Java weren’t complex enough, Unicode output is equally troublesome. You already know that PrintStreams
like System.out just chop off the high byte of a Unicode character. Although it varies from platform to platform, different output
classes in the java package either chop off the high byte like PrintStream or output \u escapes.
UTF8
To summarize what you have learned so far, characters in Java source code are 8-bit ISO Latin-1 characters. Internally, Java
translates these characters and any embedded \u escapes into 16-bit Unicode characters.
Using 16-bit characters is relatively inefficient, however, when almost all the text you’re working with is likely to be regular 7-bit
ASCII. Therefore, Java byte code embeds string literals in an intermediate format called “Universal Character Set Transformation
Format 8-bit form.” Since that’s way more than a mouthful, this is almost always written as the acronym UTF8.
UTF8 encodes the most common characters (the ASCII character set) in a single byte for each character. However, less-common
characters use two bytes, including the upper 128 ISO Latin-1 characters (which normally only take one byte apiece). The least
common characters of all — the upper 32,768 Unicode characters — are encoded in three bytes.
The details are as follows. Characters between 1 and 127 (\u0001 and \u007F) — that is, ASCII characters except null — are
encoded as their low-order byte. The high byte (which is just zeroes anyway) is discarded. If the Unicode character is between 128
and 28,927 (\u0080 to \u07FF) — that is, if its top five bits are zero — then it has 11 bits of data. These 11 bits are encoded as a pair
of bytes like this
1 1 0 x x x x x 1 0 x x x x x x
bits 6-10 bits 0-5
Characters in the range \u0800 to \uFFFF have a full 16 bits of data. These are encoded in three bytes, like this:
1 1 1 0 x x x x x 1 0 x x x x x 1 0 x x x x x x
bits 12-15 bits 6-11 bits 0-5
Note: This is not exactly the official UTF8 encoding. Java differs from the formal standard in that it uses two bytes to
encode the null character (\u0000) rather than one. Furthermore, the real UTF8 standard has several more formats to
handle four byte characters as well. By using a 4-byte character set, it’s no longer necessary to unify the Chinese,
Japanese, and Vietnamese scripts.
This encoding scheme is designed to be easy and quick to parse. Any byte that begins with a 0 bit is a 1-byte ASCII character. Any
byte that begins with 110 starts a 2-byte character. Any byte that starts with 1110 is a 3-byte character. Finally, any byte that starts
with 10 is the second or third byte of a multi-byte character.
The more ASCII characters in a text string, the more space that can be saved by UTF8. Pure ASCII text is only half as large in UTF8
as it is in true Unicode. In the worst case, where all characters occupy three bytes, a UTF8 string is only 50 percent larger than the
equivalent Unicode string. However, the worst case is rarely seen in practice.
The DataInputStream and DataOutputStream classes have writeUTF() and readUTF() methods to handle UTF8 data. readUTF() first
reads two bytes from the underlying stream. These are interpreted as an unsigned short specifying the number of bytes to read from
the stream (not the number of characters to read from the stream). These bytes are then read and translated from UTF8 into Unicode,
and a String containing the translated data is returned. We use this method in Chapter 4 to read the UTF8 strings stored in the
constant pool of a byte code file.
The DataOutputStream writeUTF(String s) method writes a Unicode string onto the underlying output stream after translating the
string to UTF8 format. The string is preceded by an unsigned short that gives the number of bytes that will be written.
Boolean
The final primitive data type is the only one that cannot be interpreted as a number. This is the boolean. A boolean has two possible
values: true and false. In Java source code, these are boolean literals. They are not the same as 1 and 0. They are not the same as the
strings "true" and "false." They are simply true and false. That’s all.
At the level of the virtual machine, things are a little different. The virtual machine does not have instructions that operate on boolean
data. Instead, expressions that involve booleans are compiled using integer instructions. The integer constant 1 is used to represent
true, and the integer constant 0 is used to represent false. Don’t try to take advantage of this when writing Java source code, though.
It won’t work.
However, for the purposes of efficiency, Java does allow arrays of booleans to be stored more compactly than arrays of ints. Sun’s
virtual machines make arrays of booleans out of arrays of bytes. In these arrays, true is 01 and false is 00. Other implementations are
free to use even more compact representations for boolean arrays, perhaps as little as one bit per value.
Cross-Platform Issues
The preceding section described how primitive data types are represented in Java. This matches fairly closely how numbers are
represented on Sparc-Solaris systems. This shouldn’t be surprising, given that Java was created by Sun Microsystems programmers
who were accustomed to Sparc-Solaris systems.
However, not all systems represent data in the same way. Most annoyingly, roughly half of computer architectures are Little-Endian
rather than Big-Endian. (Little-Endian and Big-Endian architectures are discussed shortly). Furthermore, some programming
languages allow the use of unsigned numeric quantities. And although Java’s native integer format is 32 bits, many other systems
prefer 16-bit or 64-bit ints. Although Java is supposed to be above such concerns, when you have to deal with legacy data from
programs written in other languages, you need to be aware of these differences.
Byte order
Which two mighty powers have, as I was going to tell you, been engaged in a most obstinate war for six and
thirty moons past. It began upon the following occasion. It is allowed on all hands, that the primitive way of
breaking eggs, before we eat them, was upon the larger end: but his present Majesty’s grandfather, while he was
a boy, going to eat an egg, and breaking it according to the ancient practice, happened to cut one of his fingers.
Whereupon the Emperor his father published an edict, commanding all his subjects, upon great penalties, to
break the smaller end of their eggs. The people so highly resented this law, that our histories tell us there have
been six rebellions raised on that account; wherein one Emperor lost his life, and another his crown. These civil
commotions were constantly fomented by the monarchs of Blefuscu; and when they were quelled, the exiles
always fled for refuge to that empire. It is computed that eleven thousand persons have, at several times, suffered
death, rather than submit to break their eggs at the smaller end. Many hundred large volumes have been
published upon this controversy: but the books of the Big-Endians have been long forbidden, and the whole party
rendered incapable by law of holding employment. During the course of these troubles, the Emperors of Blefuscu
did frequently expostulate by their ambassadors, accusing us of making a schism in religion, by offending against
a fundamental doctrine of our great prophet Lustrog, in the fifty-fourth chapter of the Blundecral (which is their
Alcoran). This, however, is thought to be a mere strain upon the text: for the words are these: That all true
believers shall break their eggs at the convenient end: and which is the convenient end, seems, in my humble
opinion, to be left to every man’s conscience, or at least in the power of the chief magistrate to determine. Now
the Big-Endian exiles have found so much credit in the Emperor of Blefuscu’s court, and so much private
assistance and encouragement from their party here at home, that a bloody war has been carried on between the
two empires for six and thirty moons with various success; during which time we have lost forty capital ships,
and a much greater number of smaller vessels, together with thirty thousand of our best seamen and soldiers; and
the damage received by the enemy is reckoned to be somewhat greater than ours.
I made an implicit assumption in the preceding section: that the leftmost byte of a multi-byte number is the most significant one. Of
course, spatial concepts like left and right really don’t apply to computer memories. In this context, left means lower in memory, and
right means higher. Of course, lower and higher are also spatial terms. By lower, I mean “has a smaller address,” and by higher, I
mean “has a bigger address.” Thus, if the bytes in a computer memory with n bytes of memory are organized from byte 0 to byte n-1,
then byte 0 is the lowest, or leftmost, byte and byte n-1 is the highest, or rightmost, byte.
We associate left with lower addresses in memory because computer programs start executing the instruction at a lower address and
then proceed through the instructions to a higher address. In other words, first the instruction in byte 0 is executed, and then the
instruction at byte 1, and then the instruction at byte 2, and so on.
Note: This is a little over-simplified. Not all bytes contain instructions; not all instructions are one byte long (though they
are in Java); and some instructions jump backward or forward in memory. However, none of this changes the point I’m
making here about associating lower addresses in memory with left and higher addresses with right.
When people who speak English write sequences of numbers they automatically put 0 on the left as shown here:
0 1 2 3 4 5 6 7 8 9 10 11
Because English is a left-to-right language and most of the people who developed the first computers spoke English, the spatial
concept of left came to be implicitly associated with lower addresses in memory. If the first digital computers had been invented in
Arabic- or Hebrew-speaking cultures, which use right-to-left scripts, we’d probably speak of byte 0 as the rightmost byte.
Consider the number 6401. This is shorthand for six thousands, four hundreds, zero tens, and one one. The leftmost digit, 6, is the
most important. It tells you to within a thousand how big the number is. Subsequent digits improve on the precision, but don’t
change the big picture. In jargon, it’s said that 6 is the most significant digit. Similarly, the rightmost digit, 1, is the least significant
digit.
The most significant digits are read first. Therefore, this is a Big-Endian number system. The big end of the number (the thousands)
comes before the little end (the ones) of the number. This assumption seems to be perfectly reasonable unless and until you
encounter a script in which numbers are stored differently.
A number system in which 6401 means 6 ones, 4 tens, 0 hundreds, and 1 thousand is called Little-Endian because the least
significant digits come first. There’s no reason why 6401 couldn’t mean 6 ones, 4 tens, 0 hundreds, and 1 thousand. That’s just not
the way European scripts count. There’s no mathematical reason for Big-Endian numbers. It’s purely a convention enforced by
centuries of common practice. It’s no more right or wrong than the grammatical convention that adjectives tend to come before the
nouns they modify. In English and many other languages, adjectives come first. In Latin and many other languages, the nouns come
first. Neither is right or wrong. They’re just different.
Bringing this discussion back to the level of computers, recall that a Java int can be thought of as made out of four hexadecimal
digits. For example, decimal 6401 is 0x1901. Java follows a Big-Endian scheme. The most significant digit comes first, followed by
the second most significant digit, followed by the third most significant digit, followed by the least significant digit.
Macs and most UNIX machines, including Sun’s, also support a Big-Endian architecture, where the digit with the highest place value
in a number is in the leftmost (lowest addressed) byte in the number. However, computer architectures based around the Intel X86
and VAX architectures do things exactly the opposite way. Those machines are Little-Endian; the least significant byte in a number
comes first. On an X86 system, the decimal number gets laid out in memory as 1091.
Now let’s suppose we have to store the 4-byte integer 1,870,475,384 in this memory. All computer architectures would use four
contiguous bytes. First, the integer is converted into its hexadecimal form, 6F7D3078; each 2-digit pair is exactly one byte. Working
from the bottom up, as is customary in a stack, the first byte can go to address A, the second byte to address A+1, the third to A+2,
and the fourth to A+3. Figure 2-2 shows this arrangement.
Figure 2-2 The number 0x6F7D3078 stored at address A in memory in Big-Endian order.
This is a classic Big-Endian ordering of bytes. However, not all architectures do it like this. In particular, X86 and VAX architectures
use a Little-Endian ordering. They put the most significant byte at address A+3, the second most significant byte at address A+2, the
third most significant byte at address A+1, and the least significant byte at address A. Figure 2-3 shows this arrangement.
As long as you’re on only one computer system, you don’t need to worry about this. All the routines are designed to work with the
native data format. However, as soon as you start trying to transfer data between systems, you need to worry about converting
between byte orders. Otherwise, the integer you write to a file in Big-Endian format on your Sun as 6F7D3078 (1,870,475,384) will
be read in Little-Endian format as 78307D6F (2,016,443,759) on your PC — not the same thing at all.
Note: Some older computer systems used neither Big-Endian nor Little-Endian byte orders. DEC’s PDP-11 wrote 4-byte
integers in this order: second-least-significant byte, least-significant byte, most-significant byte, and second-most-
significant byte. Other computers did even stranger things. Fortunately, these architectures have all died out, and we’re
now left to deal with only the confusion between Little-Endian and Big-Endian.
Java was first designed by Big-Endian engineers at Sun Microsystems. It was also designed for the Internet, where almost all
protocols specify Big-Endian byte orders. Therefore, it should come as no surprise that Java’s virtual machine uses Big-Endian
format for all data types. Little-Endian systems, like the X86, have to translate the Big-Endian data in Java byte code into their native
Little-Endian format before executing it.
Secret: You need to worry about byte order only when you’re reading data that comes from a Little-Endian source. The
readByte(), readShort(), readInt(), readLong(), readFloat(), and readDouble() methods of java.io. DataInputStream all
assume the data is Big-Endian. Similarly the writeByte(), writeShort(), writeInt(), writeLong(), writeFloat(), and
writeDouble() methods of java.io.DataOutputStream write Big-Endian data. To read Little-Endian data in Java, you have
to read each byte separately and then reconstruct the int or long from the bytes that make it up. To write Big-Endian data,
you have to break the ints or longs apart into bytes and then write the bytes separately. There are several ways to
accomplish this, but the most efficient use the bit-level operators discussed later in this chapter. I revisit this topic there.
Unsigned integers
Many traditional programming languages, notably C, allow the use of unsigned quantities. An unsigned number uses its high-order
bit for data so it can count twice as high as a number that has to reserve one bit for the sign. However, it can only count positive
numbers, not negative numbers. Recall that the largest signed byte is 01111111, which is 127 in decimal. 11111111 is not 255 but
rather -128. However, by reading 11111111 as an unsigned quantity, the first 1 bit is interpreted as 128, not the - sign. Thus, as
unsigned quantity, 11111111 is indeed 255. On the other hand, there’s no way to express negative numbers as unsigned numbers.
All Java numeric data types except char use signed integers exclusively. However it’s not unlikely that you’ll run across data from
programs written in other languages that do have unsigned integers. java.io.DataInputStream has two methods that read unsigned
quantities. readUnsignedByte() reads a single byte off the stream and returns an int between 0 and 255. An int is returned instead of a
byte or a short because a byte can go only as high as 127, whereas an unsigned byte can go as high as 255. Similarly
readUnsignedShort() reads two bytes from the input stream and returns an int between 0 and 65,535.
There is no similar readUnsignedInt() method. If you want to, it’s easy enough to write one yourself. You’ll need to read four bytes
and return a long between 0 and 4,294,967,295. Again, the most efficient way to do this uses bit-level operators, so we’ll defer the
details until the end of this chapter.
An unsigned long — that is, an 8-byte unsigned integer — is relatively uncommon in practice. No primitive Java data type is large
enough to handle unsigned longs. You can, however, use the java.math.BigInteger class instead.
Integer widths
You’ve probably heard a lot of hype about 32-bit computing and 32-bit clean code. You’ll be hearing more about 64-bit platforms in
the near future, if you haven’t already. What’s being referred to is, very roughly, the preferred size of an integer on a given computer
architecture and the number of bits that can be transferred from main memory to the CPU in one clock cycle. Generally, the higher
the number of bits, the faster the computer will run. However, you need to rewrite (or at least recompile) the software to
accommodate the proper bit width before you can see the performance gain.
Much legacy code is written in languages like C that do not guarantee the width of an integer. The same C program may use 32-bit
ints on a Sparc, 16-bit ints on a Mac, and 64-bit ints on a DEC Alpha. Although these all have Java equivalents, you have to know
which one you’re dealing with before you write the code to handle it! Trying to read 16-bit ints with Java’s readInt() method is a sure
path to failure.
There’s no guaranteed way to look at a file in the absence of outside information and tell solely from the contents of the file whether
it was written using 16-bit integers or 32-bit integers. Similarly, you can’t tell whether or not it uses Big-Endian or Little-Endian
data. In an ideal world, you’d have access to a specification that describes the data format used. If you don’t, perhaps you have
access to the source code that was used to write the file. If not, you’ll have to do some testing. Try to read the file as 16-bit ints. Do
the results make sense? What if you read it as 32-bit ints? Do those results make sense? If you seem to have an excessive number of
zeroes appearing in your data, especially if they tend to alternate with non-zero values, that may indicate that you are reading the data
using too short an integer. For example, if the data file is full of numbers mostly between 10 and 1000, then if it’s written with 32-bit
ints, the high two bytes of each int will be zero.
With seven different numeric types that may be freely intermixed in expressions, it’s important to understand the rules by which this
intermixing takes place. Java converts between primitive data types in expressions, in assignment statements, as a result of explicit
casts, and during method invocations. You need to understand when conversion can occur and what happens when it does.
Using a cast
Java enables you to explicitly change the type of a value using a cast. A cast is just the name of the type to which you wish to change
the value, enclosed in parentheses. For example, suppose you’ve read a byte into the byte variable b, perhaps using
DataInputStream’s readByte() method. Then you can cast that variable to the int type like this:
int n = (int) b;
This doesn’t permanently change the type of b. It just makes a temporary copy of the value of b and puts it in an int. This int is then
assigned to the int variable n.
The second place in which conversion of primitive types takes place is in arithmetic expressions. Expressions range from simple
ones, like a + b, to considerably more complex ones such as 1.65 * (32 / -9.8 - c++)/0.65. The expression is evaluated using the
widest type present in the expression, where doubles are wider than floats, which are wider than longs, which are wider than ints.
Thus, if any of the operands are doubles, all operands are promoted to doubles. If no operands are doubles but some are floats, then
all operands are promoted to floats. If no operands are floats or doubles but some are longs, then all operands are promoted to longs.
Finally, if an expression contains no floats, doubles, or longs, then all operands are promoted to ints. All arithmetic in Java uses at
least ints. Shorts, bytes, and chars are never used directly in arithmetic expressions.
The third place in which conversions take place is in assignment statements; that is statements like
long a = 3 + 4;
In this example 3 is an int, 4 is an int, and the result of their addition is the int value 7. This must be promoted to a long before being
assigned to a. Conversions in the other direction may lose information. Not all longs have equivalent int values. For example,
5294967295L is a valid long, but it’s more than two times larger than the largest int:
int n = 5294967295L;
If you try to assign 5294967295L to an int variable, you get the compile-time error Error: Incompatible type for
declaration. Explicit cast needed to convert long to int. The compiler sees that you may lose
information and warns you about it. However, the compiler isn’t that smart. The following assignment, which does not lose
information, also causes a compiler error:
int m = 3L;
In both of these cases, you can tell the compiler that you’re aware of the problem, that you accept that your assignment may lose
information, and that you want it to go ahead anyway. You do this with an explicit cast to the type on the left side. For example:
int m = (int) 3L;
int n = (int) 5294967295L;
This tells the compiler that you know what you’re doing, that you’ve given thought to whether this cast will lose data. Java tries to
prevent you from performing operations that may lose data, but it does allow you to do so if you use a cast to tell it that you know
what you’re doing.
The final place where conversions take place is in method calls. Suppose you try to call MethodA(24). The compiler first tries to find
a perfect match, a version of MethodA that takes as an argument a single int. However, if it fails in this effort, it will next look for a
MethodA that takes a long as an argument. If it finds one, it promotes 24 to 24L and calls MethodA(long). Failing to find a MethodA
that takes a long, Java next looks for one that takes a float. Failing to find that, it looks for one that takes a double. Only if it can’t
find any of these will Java produce a compile-time error.
Now that we’ve seen when conversions may take place, let’s investigate how. Some conversions, such as an int to a long, are easy
and never lose information. Others, such as a long to an int, are trickier because not all longs have int equivalents. For example,
suppose that a byte variable b holds the value 92. In binary notation, this is 01011100. Because an int needs 32 bits, three extra zero
bytes are added to the front of b, making it 000000000000-00000000000001011100.
Now suppose instead that the value of b is -92. Using two’s complement arithmetic, we see that the binary expansion of -92 is
10100011 + 00000001 = 10100100. Now if you just attach three bytes of zeroes on the left side of this number, you get
00000000000000000000000010100100, which is not -92 (since the sign bit is zero, the number must be positive) but rather 164, not
the same thing at all. In fact, it’s not even off by a sign. If that were the problem, it would be simple enough to change the leftmost
bit to 1. However, here that gives you -164, which isn’t -92 any more than 164 is.
On the other hand, look what happens if you extend -92 with three bytes full of ones. You get
11111111111111111111111110100100. This is obviously a negative number since the leftmost bit is one. Using two’s complement
arithmetic to find out which number it is, you invert the number and add one:
00000000000000000000000001011011
+00000000000000000000000000000001
00000000000000000000000001011100
which, lo and behold, is 92! Thus, the proper way to convert an integer type to a wider format is sign extension. That is, take
whatever bit is in the sign bit and add as many extra bytes as you need filled with that bit. This works for other widening casts
between integer types as well. For example, to change a positive int to the equivalent long, just add four bytes of zeroes to the front.
To change a negative int to the equivalent long just attach four more bytes of ones to the front of the int. Performed in this fashion,
widening integer casts — that is, casts that go from a smaller type to a larger type — never lose information.
The same cannot be said for narrowing casts. A narrowing cast moves from a wider type, like int, to a narrower type, like byte. To do
this, the extra bytes are just cut off the front of the wider type. Thus, to move from the int 92 to the byte 92, remove the first three
bytes from 000000000000-00000000000001011100, leaving 01011100. This cast doesn’t lose information, but other casts can. For
example, the int 192 is 00000000000-000000000000011000000. If you cast this to a byte by removing the first three bytes, you get
11000000. Notice the sign bit. This is a negative number, specifically -64. There is no easy way around this problem. The numbers
you get in a narrowing cast are not guaranteed to make sense. The simple fact is that you cannot fit 192 into a signed byte.
The two basic rules for conversion between integer data types are as follows:
1. If the type to be converted to is wider than the type you’re converting from, sign extend the narrower type.
2. If the type to be converted to is narrower than the type you’re converting from, truncate the most significant bytes of the
integer you’re converting.
Conversions to and from the char type behave similarly, once you take account of the fact that char is unsigned. To convert a char to
a byte, the high-order byte is truncated. To convert a char to a short, the char is left as is, but is now interpreted as a signed 2-byte
integer. To convert to an int or a long, the char is sign extended by two or six bytes respectively. This may produce a negative
number where there wasn’t one before if the char value is greater than 32,767 — that is, if its high-order bit is one.
To convert a byte to a char, the byte is sign extended one byte. To convert a short to a char, the short is merely reinterpreted as a
signed, 2-byte integer. Finally, to convert an int or long to a char, all but the least-significant 16 bits are truncated. Although
converting a char to a short, int, or long may play funny games with the sign, converting it back will return the original char.
The rules for conversions to and from floating-point numbers are more complex. A float can be cast to a double with no loss of
precision whatsoever. Double to float conversion presents some problems, though. Some doubles can be exactly represented as
floats, but some are too large, some are too small, and some have more precision (that is, a longer mantissa) than a float allows. If the
absolute value of the double is larger than can fit in a float, the float becomes infinity — positive or negative depending on the sign
of the double. If the absolute value of the double is smaller than can fit in a float (that is, closer to zero), the float becomes zero —
positive or negative depending on the sign of the double.
Floats and doubles that are small enough to be represented as ints must fall between two ints; that is, there is an int value larger than
the float and an int value smaller than the float. The float is rounded to the int in the pair between which it falls that is closest to zero.
Thus, 7.5 is rounded to 7; 7.6 is also rounded to 7, but -7.5 is rounded to -7, not to -8. If the float or double is too large to be
represented as an int, for example 6.73E14, then it is rounded to the largest possible int, 2,147,483,647. Similarly, if the float is too
small and negative, for example -6.73E14, then it is rounded to the smallest possible int, -2,147,483,648. NaN is rounded to zero.
Rounds to longs behave similarly except that the largest and smallest values are quite a bit larger.
Conversions of floats and doubles to shorts and bytes involve a two-step procedure. First the float or double is converted to a double,
as described earlier in this chapter. Then the int is converted to a byte or short in the normal way, by truncating the excess bytes in
the int. Thus, casting the float 7.5 to a byte results in the value 7. However, casting 175.5 to a byte results in the value -47. This
occurs by first rounding 175.5 to 175, 0x000000AF, and then by truncating this to AF, 10101111 in binary. Of course, a byte is
signed, so this is equal to -47.
I can think of little reason to want to convert a float or a double to a char, but you can if you need to. The conversion takes place
much as with conversions to shorts: the float or double is first converted to an int, which is then converted to a char.
Bit-Level Operators
The 13 bit-level operators are among the more obscure in Java. They nonetheless have their uses. The bitwise operators operate on a
number or boolean at the bit level, generally by comparing the bits in two quantities and returning a result that depends on the bits in
each. The single exception is ~, the NOT, or complement, operator. It takes a single argument and inverts all its bits. The bitshift
operators take two operands: the number to be shifted and the number of places to shift it. Except for ~, these operators have “operate
and assign” equivalents as well. Table 2-5 lists all the bit-level operators in Java.
Operator Meaning
& AND
| OR
^ Exclusive OR
~ NOT (complement)
<< Shift bits left
>> Shift bits right
>>> Shift bits right without sign extension
&= AND and assign
|= OR and assign
^= Exclusive OR and assign
<<= Shift bits left and assign
>>= Shift bits right and assign
>>>= Shift bits right without sign extension and assign
Some terminology
We’ll need some shorthand to discuss these operators. First, given a value with n bits, the rightmost, least-significant bit is bit 0. The
second-rightmost bit is bit one, and so on, up to the leftmost and most significant bit, which is bit n-1. For example, the byte value
37, 00010101 in binary, would have bits shown in Figure 2-4.
Next, when I write that a bit is “set,” or “on,” that means the bit is 1. When I write that a bit is “not set,” “unset,” or “off,” that means
the bit is 0. You’ll also hear these states referred to as “true” and “false” in other books, but I avoid that terminology here to avoid
confusion with the boolean literals.
Finally, note that a lot of the examples in this book will be with bytes, simply because it’s easier to follow what’s going on when you
only have to keep track of eight bits. However, just as Java performs arithmetic only on int and larger data types, and promotes the
operands as necessary, so too will it promote the operands of a bitwise operator and return an int or larger result. For example, even
if b1 and b2 are bytes, b1 & b2 is an int; both b1 and b2 are promoted to ints before the bitwise and is performed.
Bitwise operators
The bitwise operators — &, |, and ^ — combine two numbers according to their bit patterns. The bitwise not operator ~ inverts a
single number’s bit pattern.
The & operator is the bitwise AND operator. It takes two numeric arguments, compares their bits, and sets the bits in the result that
are set in both of the arguments. For example, let b1 be a byte with value 78 and b2 be a byte with the value -23. In binary, 78 is
01001110 and -23 is 11101001. Lay these values on top of each other as shown in Figure 2-5. The result, shown in the bottom row,
is 01001100, that is, 76.
The bits that are equal to one in both 78 and -23 are equal to one in the result. All other bits are zero.
As mentioned earlier, Java actually performs this calculation using 32-bit ints. Because the high-order three bytes of a positive int are
just full of zeroes, the real result of 78 & -23 must be 0000000000000000000000000-1001000. If either argument of & has a zero bit
in a particular position, that bit must be 0 in the result, regardless of the value of the bit in the second argument. Therefore, 0 &
anything is always 0.
The & operator can also be used with two booleans: true & true is true, true & false is false, and false & false is false. At the level of
the virtual machine, the boolean value true is the int 00000001 and false is the int 00000000. Thus, true & true is the same as
00000001 & 00000001 equals 00000001 or true. Conversely, false & false is 00000000 & 00000000 equals 00000000 or false. And
finally, true & false is 00000001 & 00000000 equals 00000000 or false.
This is often used to avoid short-circuiting expression evaluation. Suppose isConditionOne() and isConditionTwo() are methods that
return booleans and have some side effect such as printing output on System.out. Now suppose you write this statement:
If isConditionOne() returns false, then isConditionTwo() is never called. Because isConditionOne() is known to be false, Java knows
the result will be false, regardless of the value of isConditionTwo(). This can be a problem when isConditionTwo() has side effects,
and you need it to be called regardless of condition one. To force isConditionTwo() to be called, use the bitwise & instead. That is
The truth value of ( isConditionOne() & isConditionTwo() ) is the same as the truth value of ( isConditionOne() && isConditionTwo
() ), but now both methods will be called.
The | operator
The | operator is the bitwise OR operator. It takes two numeric arguments, compares their bits, and sets the bits in the result that are
set in either or both of the arguments. For example, let b1 be a byte with value 78 and b2 be a byte with the value -23. In binary, 78 is
01001110 and -23 is 11101001. Lay these values on top of each other as shown in Figure 2-6. The result, shown in the bottom row,
is 11101111, that is -17.
The bits that are equal to one in either 78 or -23 or both are equal to one in the result. All other bits are zero.
Of course, Java actually performs this calculation using 32-bit ints. Because the high-order three bytes of a positive int are just full of
zeroes, the real result of 78 & -23 is 11111111111111111111111111101111. If either argument of | has a one bit in a particular
position, that bit must be 1 in the result, regardless of the value of the bit in the second argument.
The | operator can also be used with two booleans: true | true is true, true | false is true, and false | false is false.
The AWT sometimes uses this to set a series of flags. If you have an item that has up to 32 boolean characteristics, then you can stuff
all the values of those characteristics into an int.
For example, consider the java.awt.Font class. To create a new font, you use this constructor:
The name is the name of the typeface, like Times or Arial. The size is the size of the font in points, such as 12 or 24. The style,
however, is one of a special set of mnemonic constants. These constants are
Font.BOLD = 1
Font.PLAIN = 0
Font.ITALIC = 2
You can pass one of these constants in the style argument of the Font constructor to get that style. However, what if you want a Font
that is both bold and italic? Then, you pass Font.BOLD | Font.ITALIC. This means that the bold bit and the italic bit are both set in
the style argument. Notice that Font.BOLD is 00000001 whereas Font.ITALIC is 00000010. Each bit in the number is a binary flag
indicating the value of the binary characteristic; for example, is this or is this not bold? Other classes that use this scheme can have
many more such constants, all of which are powers of two: 4, 8, 16, 32, 64, and so on. Each power of two is a 32-bit int with exactly
one bit set and the rest unset.
As with &, | can also prevent the short-circuiting of expression evaluation. Consider the statement
The truth value of ( isConditionOne() | isConditionTwo() ) is the same as the truth value of ( isConditionOne() || isConditionTwo() ),
but now both methods are called.
The ^ operator
The ^ is the bitwise EXCLUSIVE-OR operator. The operator | does not behave like many people expect, based on its English
meaning. Many people think the “A or B” is true if A is true and B is not true, or vice versa, but that “A or B” is not true if both A
and B are true. ^ is the bitwise equivalent of this idea. The ^ operator takes two numeric arguments, compares their bits, and sets the
bits in the result that are set in exactly one of the arguments.
Returning to the example where b1 is a byte with value 78 and b2 is a byte with the value -23, lay these values on top of each other
as shown in Figure 2-7. The result, shown in the bottom row, is 10100111-89.
The ^ operator can also be used with two booleans: true ^ true is false, true ^ false is true, and false ^ false is false.
The ~ operator
The ~ is the bitwise NOT or complement operator. It is unary; that is, it acts on a single number or boolean, and it flips all the bits in
that value. As a result, all ones turn to zeroes and zeroes turn to ones. Figure 2-8 shows 78 and ~78.
Assignment operators
The &=, |=, and ^= operators behave like their arithmetic cousins, *=, +=, -=, %= and /=. In other words, they combine the value on
the left side of the operator with the value on the right side, and then assign it to the left side. For example:
int a = 78;
a &= -23;
This makes a equal to 76. |= and ^= behave similarly except they use bitwise OR and bitwise XOR respectively.
The bit shift operators shift the bits in an integer type by a specified number of places to the right or left. Bit shift operators cannot be
used on floats, doubles, or booleans. For example, << is the left shift operator. The integer 78 is
00000000000000000000000001001110 in binary. Table 2-6 shows the result of shifting it progressively leftward. Notice that at each
step the pattern of ones and zeroes appears to move one bit further left.
Table 2-6Left-shifting 78
78 00000000000000000000000001001110
78 << 1 = 156 00000000000000000000000010011100
78 << 2 = 312 00000000000000000000000100111000
78 << 3 = 624 00000000000000000000001001110000
78 << 4 = 1248 00000000000000000000010011100000
78 << 5 = 2496 00000000000000000000100111000000
78 << 6 = 4992 00000000000000000001001110000000
78 << 7 = 9984 00000000000000000010011100000000
78 << 8 = 19,968 00000000000000000100111000000000
78 << 9 = 39,936 00000000000000001001110000000000
78 << 10 = 79,872 00000000000000010011100000000000
78 << 11 = 159,744 00000000000000100111000000000000
78 << 12 = 319,488 00000000000001001110000000000000
78 << 13 = 638,976 00000000000010011100000000000000
Also notice that at each step, the value of the number is doubled. A 1-bit shift left is exactly equivalent to multiplication by two.
Depending on the compiler, the virtual machine, and the CPU, it may be mildly quicker to shift an int to the left by the appropriate
number of bits rather than to multiply by two. Similarly, shifting an int to the right can replace dividing by two or a power of two.
However, this optimization may well not be worth the decrease in the legibility of your code, even on platforms where it makes a
difference in performance.
What happens when the pattern of ones reaches the left side? Does it wrap around? No. The ones just march off to the left as the
right side fills with zeroes. Note that once you hit 25 left shifts, you lose the multiplication by two property and drop over into
negative numbers. If you had started with a larger number, this might have happened sooner. From that point on, the results bear
little numerical relation to the original 78. Table 2-7 demonstrates.
However, if you keep going, something interesting happens. The next shift, by 32, appears to bring the number back, as Table 2-8
demonstrates.
Table 2-8 should look familiar. Except for the number of bits by which 78 is shifted, it’s an exact copy of Table 2-6. Did it just take a
little extra time to wrap around? Not exactly. Java limits the right side of the shift operator to five bits (six bits if the left side is a
long). Extra bits are truncated. This means that you can only really shift an int (or a byte, or a short) between 0 and 31 bits. Longs
can be shifted between 0 and 63 bits. If you try to shift by more than that, Java throws away the higher-order bits. Thus, in the last
line of Table 2-8, 78 is really being shifted by 45 - 32 = 13 bits, not by 45 bits.
78 << 32 = 78 00000000000000000000000001001110
78 << 33 = 156 00000000000000000000000010011100
78 << 34 = 312 00000000000000000000000100111000
78 << 35 = 624 00000000000000000000001001110000
78 << 36 = 1248 00000000000000000000010011100000
78 << 37 = 2496 00000000000000000000100111000000
78 << 38 = 4992 00000000000000000001001110000000
78 << 39 = 9984 00000000000000000010011100000000
78 << 40 = 19,968 00000000000000000100111000000000
78 << 41 = 39,936 00000000000000001001110000000000
78 << 42 = 79,872 00000000000000010011100000000000
78 << 43 = 159,744 00000000000000100111000000000000
78 << 44 = 319,488 00000000000001001110000000000000
78 << 45 = 638,976 00000000000010011100000000000000
In this example, we used an int. You can also shift bytes, shorts, chars, and longs. Bytes, chars, and shorts are promoted to ints before
being shifted. Floats, doubles, and booleans cannot be shifted.
If you really need to create a float from a series of bits, there are a couple of workarounds. You can shift the bits around in an int
and use the static java.lang.Float.intBitsToFloat() method to convert the int into a float. For example, suppose data is a byte array
with four components that correspond to the four bytes in a float. (You’ll see exactly this in Chapter 4.) You can read the float out
of the byte array by first shifting the bytes into an int called bits and then calling the java.lang.Float. intBitsToFloat(int bits)
method like this:
You can make doubles from longs in a similar fashion with the java.lang.Double.longBitsToDouble(long bits) method.
Alternately, you can construct a ByteArrayInputStream from the byte array, chain the ByteArrayInputStream to a
DataInputStream, and then call the readFloat() or readDouble() method of the DataInputStream. For example,
In most Java implementations, this is less efficient than the first alternative. However, it produces slightly more intelligible code.
The >> operator shifts numbers to the right with sign extension. This means that vacated bits on the left are filled with the sign bit: 0
for a positive number or 1 for a negative number. Otherwise, right shifts carry the same caveats as left shifts: The left side must be an
integral type and will be promoted to an int if necessary before shifting. The left side must be between 0 and 31 (0 to 63 if the left
hand side’s a long) and will be truncated to that value if necessary. For example, the int -23 is, in binary notation,
11111111111111111111111111101001. Table 2-9 shows what you get when this is right shifted by various numbers of bits. Note
that the vacated spots are filled with sign bits and that right shifting is equivalent to division by two.
Sometimes you don’t want to fill with the sign bits, but rather with 0. The >>> operator does an unsigned shift right. In other words,
it fills the vacated spaces with zeroes regardless of the sign bit. Table 2-10 demonstrates this.
-23 11111111111111111111111111101001
-23 >> 1 = 2,147,483,636 01111111111111111111111111110100
-23 >> 2 = 1,073,741,818 00111111111111111111111111111010
-23 >> 3 = 536,870,909 00011111111111111111111111111101
-23 >> 4 = 268,435,454 00001111111111111111111111111110
-23 >> 5 = 134,217,727 00000111111111111111111111111111
-23 >> 6 = 67,108,863 00000011111111111111111111111111
The >>=, <<=, and >>>= behave as you might expect, shifting the left argument by the number of bits specified in the right
argument and in the direction specified by the operator, and then assigning the result to the left side.
Little-Endian data
To read Little-Endian data, you first read the necessary number of bytes into an array. Then you use the << bit shift operator and the |
operator to put the parts of the Little-Endian number back together in the right order.
Longs are just the same except you have to use an 8-byte buffer and put eight pieces back together.
To write Little-Endian data, you create a buffer for the bytes in an int. The bytes are extracted from the int by a simple cast. Recall
that casting an int to a byte truncates the int to its least significant byte. Before the cast is done, the right shift operator >> moves the
needed byte into position in the least significant byte.
Unsigned integers
java.io.DataInputStream has methods to read unsigned bytes and unsigned shorts, but nothing to read an unsigned int. To do that,
you must read four bytes and use them to construct the lower four bytes of a long. The upper four bytes of the long will be zero. For
example:
It’s necessary to combine the result with 0xFFFFFFFF using a bitwise and to make sure that none of the bytes were sign extended
into negative numbers when left-shifted.
Bit shift operators are fairly obscure. One of the few areas of Java where they’re useful is working with images and image filters.
Java images are built with a 32-bit color model. Each color has four channels: alpha, red, green, and blue. The alpha channel
represents transparency. The other three channels are the primary colors for an additive color system. Each of the four channels has a
value from 0 to 255 (in other words, one unsigned byte). For the color channels, the higher the value, the brighter the color. For the
alpha channel, the higher the value, the more opaque the image is.
Note: Java 1.0’s support for transparency is mainly theoretical. A value of 255 is fully opaque. Anything less is 100
percent transparent (invisible).
Figure 2-9 shows a color that is 50 percent gray. The alpha channel is 255 (11111111), which is fully opaque, while each of the red,
green, and blue channels is set to 127. This means the color is equal to the integer 111111011111110111111101111111. The integer
value has little meaning here, though; it’s the individual bytes that matter.
When the red channel, green channel, and blue channel have the same value, the resulting image varies from black (all three
00000000) to white (all three 11111111). It passes through various shades of gray in between. By varying the colors
disproportionately, you can produce the different colors of the visible spectrum. For example, pure blue is 11111111000000-
001111111100000000.
So how do you create these colors? It’s simple, really. Just initialize ints to the values you want for each of the four channels, shift
them into place, and combine them with the bitwise OR operator, |. For example, to create a pure blue, do the following:
If you prefer, you can combine these on one line. For example, to create the 50 percent gray of Figure 2-9, use this command:
int halfgray = (255 << 24) | (127 << 16) | (127 << 8) | 127;
Summary
In this chapter you learn how a computer stores numbers. You learn what a place-value number system is, and about the binary and
hexadecimal place-value number systems computers use. You learn how the primitive Java data types like int and float are laid out in
memory and how this affects operations with those types.
You also learn how Java stores characters and the different character sets used for this purpose, particularly ASCII, ISO Latin-1,
Unicode, and UTF8. You learn when and for what purposes these different but related character sets are used and how to convert
from one to another.
Finally, you learn how to use the bit-level operators to operate on numbers at a very low level. The bitwise operators combine values
in memory, while the bitshift operators move the bits in data back and forth.
Sorry!
Bad Bookmark?
If you had this address bookmarked, we appologize for the inconvenience. We recently
completed some necessary reorganization of the site. Please update your bookmarks.
You can return to the main page of the site you came from:
Or you can:
❍ check the URL you typed for spelling and capitalization and try it
again
If you clicked on a link inside our site, please contact either ITKnowledge or The Perl
Journal (please select the site you came from) with the URL of the page containing the
incorrect link. If you got here from another site, please contact the administrator of the page
which contains the error. Thank you!
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
Chapter 3
Classes, Strings, and Arrays
T he last chapter explored Java’s primitive data types. This chapter explores Java’s reference data types. A primitive data type is
one whose value is stored directly in memory. A reference data type stores only a reference to the place where the actual data can be
found. There are two reference data types: objects and arrays. Objects and arrays are normally explained very abstractly and at a very
high level. It’s my goal in this chapter to explain them very concretely and at a very low level. By understanding the low-level
structure you can make sure you’re that working with Java rather than against it and substantially speed up your programs.
The Heap
The heap is a large block of memory that Java uses to store objects and arrays. Memory in the heap can be allocated discontiguously.
When a new object or array is created, the space comes from somewhere in the heap. Exactly where isn’t important, or even defined.
When an object or array is garbage-collected, the memory that it occupied in the heap is freed. That is, the memory is marked as
unused and made available for reuse by other objects.
An object has two parts: its fields and its methods. Each field requires memory to hold a value appropriate to its type. Each method
requires memory to hold its arguments and return values and code. However, the memory for the method is needed only when the
method is invoked. Furthermore, methods are the same for each instance of the class. Methods therefore are allocated on an as-
needed basis in an area of memory called the stack.
double x;
double y;
double z;
// various methods...
}
This class has three double fields. Each double occupies eight bytes. Therefore, each instance of this class needs 24 bytes of memory
in the heap. If there is one 3DPoint object in existence, then exactly 24 bytes of heap memory are needed. If there are two 3DPoint
objects in existence, then 48 bytes of heap memory are needed. If there are three 3DPoint objects, then 72 bytes of heap memory are
needed, and so on.
Arrays are similar. To determine how much heap memory that an array requires, multiply the length of the array by the width of the
data type stored in the array. A float array of length 10 thus needs 40 bytes of heap memory; a char array of length 10 needs 20 bytes
of heap memory; and a byte array of length 10 needs 10 bytes of heap memory.
When a new object or array is created, the necessary amount of space is set aside for it in the heap. The new operator returns a
reference to the block of memory in the heap where the object or array is stored. The virtual machine is responsible for managing the
heap and making sure that the same block of memory is not used for two different objects or arrays at the same time.
The exact size of the heap is system-dependent. However, the heap is finite on all systems. In some Java implementations, the heap
can grow if more space is needed. On others the size of the heap is fixed when the virtual machine starts up. Nonetheless, the heap is
definitely smaller than the memory (physical or virtual) available on the host computer. If the heap fills up, the runtime system
throws an OutOfMemoryError.
Garbage collection attempts to prevent this from happening by purging objects and arrays from the heap when they’re no longer
necessary. Exactly how the garbage collector decides what can and cannot be purged from the heap is one of the topics in Chapter 6.
For now, all you need to know is that the garbage collector is quite reliable and won’t purge anything that you might actually need to
use.
Objects of different types require different amounts of memory. The more fields that an object has, the more memory that it needs in
the heap. Objects can contain other objects as fields. For example, consider this class:
Integer i1;
Integer i2
// various methods...
}
The GridPoint class contains two Integer objects. A GridPoint object does not store the Integer objects themselves in its own block of
memory; it stores only references to the Integer objects. References take up four bytes. Therefore, the GridPoint object needs eight
bytes of heap memory, regardless of how much heap memory an Integer object requires. Of course, the total memory used by a
program will include the memory used by all of the GridPoints, all of the Integers, and all of the other objects stored in the heap.
There’s a lot of confusion about whether Java does or does not have pointers. If you’ve never programmed in a pointer-based
language like C or Pascal, then you will probably never need to understand pointers. You can rest assured that Java lets you do
everything that you normally use pointers to do, especially with respect to data structures. However, if you’re accustomed to a
pointer-based language like C, then you probably need to be convinced of this statement.
What is a pointer?
A pointer is the address of a particular byte of a computer’s memory. For example, a computer with eight megabytes of memory has
8 * 1024 * 1024 = 8,388,608 bytes of memory. Therefore, the valid pointers on this system begin at zero and count up to 8,388,607.
The first byte of memory has the address zero. The last byte of memory has the address 8,388,607. With a pointer, you can inspect
the contents of any byte or group of bytes. Similarly, you can write any value you like at any point in memory. For example, in C, to
write the int 768 in the four bytes starting with byte 4,324,682, you would write
int n = 4324682;
int* m = (int*) n;
*m = 768;
No check is performed to make sure that it makes sense to put the value 768 at memory location 4324682. If you put the wrong value
in the wrong place, it can crash your program, your machine, or worse.
These sorts of bugs are common in C programs. Java has eliminated pointers in order to prevent them. Furthermore, pointers open up
many security holes, because they allow any program more or less unrestricted access to all parts of the system.
What is a handle?
A handle is a pointer to a pointer. That is, a handle points to a location in memory where the address of the actual data can be found.
The advantage of handles over raw pointers is that an object can be moved in memory and the pointer to it updated while the handle
for the program remains valid. This has significant advantages for keeping memory clear and defragmented.
For example, after a program has run for some time, many objects will have been constructed and garbage collected. This can make a
heap very fragmented, as shown in Figure 3-1. Each block is a word. The gray blocks are words in use. The white blocks are free
words. Of course, real heaps have many more words than this, but this is sufficient for a demonstration.
Suppose, with the heap in this state, that you need four words for an object. There is plenty of space in the heap, but it’s fragmented.
There is no one place where you can get four words of contiguous memory. To make space for the new object, you have to move
some of the allocated blocks around in memory. However, this can cause problems if the running program has pointers straight into
the heap. For example, consider Figure 3-2. This is the same heap, with object variables shown as ovals. The arrows are pointers into
the heap. Each object has at least one pointer (to its own data), and some have multiple pointers if they themselves contain references
to other objects. Furthermore, one object may be pointed to from several different places. This interconnected web of pointers makes
it very difficult to move objects in the heap, because you have to update all of the different pointers that can exist in hundreds of
different objects, methods, and threads.
How can the references be arranged in such a way that they don’t break when the heap is defragmented? One way to look at the
problem is that references point to areas of different sizes in the heap. If you could somehow arrange it so that every object needed
exactly the same amount of space in the heap, then fragmentation would not be a problem. As long as there was any free space at all,
it could be used.
Of course, different objects do take different amounts of space, but references always take four bytes (one word). The solution is to
insert an extra block of references between the references in your source code and the heap. When an object is moved in the heap,
only one link needs to be updated: the one between the offset table and the data in the heap. The many more pointers to the offset
table do not need to be updated. Furthermore, it’s relatively easy to find the pointers in the offset table that need to be updated. The
VM does not need to search the entire memory space of the running program looking for anything that might be a pointer.
Figure 3-4 shows this scheme. To find an object’s data, you follow the first arrow into the offset table. Then you follow the second
arrow out to the actual data in the heap.
At first glance this appears more complicated than the method in Figure 3-2. However, consider what happens when the heap is
compacted. Figure 3-5 shows the result. The object pointers don’t need to be changed. Only one pointer needs to be adjusted for each
object, not one pointer for each reference, as in the previous case. Because there’s a one-to-one relationship between filled entries in
the offset table and objects in the heap, once you’ve adjusted the pointer from the offset table to the object, you’ll never have to
adjust another pointer to the same object later. If you’re moving only one object in the heap, you can stop looking as soon as you find
the pointer to it in the offset table.
There are many optimizations that can be made to this scheme. For example, each object in the heap can contain the index of its
pointer in the offset table, so when the memory manger needs to move it, the memory manager can adjust the pointer in constant
time.
Secret: This all happens behind the scenes, so you normally don’t need to worry about it. Sun’s virtual machines use
handles, but this isn’t absolutely necessary. Microsoft’s VM implements references as pointers, not doubly indirected
handles.
Of course, double indirection is useful not only in virtual machines. This scheme, or variants of it, can be used in situations where
moving objects in the heap is very expensive but moving objects in the offset table is cheap. For example, if the heap is actually a
file on disk but the offset table is in memory, then you can reorganize the structure of a file by changing the offset table. Variations
on this scheme are used in most relational databases.
What is a reference?
Reference is strictly a Java term. There are no references in C or Pascal. A reference is an abstract identifier for a block of memory
in the heap. Furthermore, a reference has a type like string or double[]. At the level of the non-virtual host machine, references may
be implemented as handles, pointers, or something else entirely. However, references are not pointers; they are not handles; they are
merely a means of identifying a particular block of memory in the heap.
How exactly the virtual machine implements references at the level of machine code is VM-dependent and completely hidden from
the programmer in any case. Most VMs — including Sun’s — use handles, not pointers. Microsoft’s VM uses pointers rather than
handles. Other schemes are possible.
Ninety percent of the time, you can ignore the difference between a reference to an object and the object itself. However, there is
always that annoying 10 percent of the time when the difference becomes important. This 10 percent occurs mostly when passing
arguments to methods.
There are two ways to pass an argument to a method: by value and by reference. The difference is in what happens to the variable
passed in the calling method as a result of what’s done to it in the called method. For example, consider this code fragment:
int a = 7;
changeVariable(a);
System.out.println(a);
What value gets printed — 7 or 10? If the argument a is passed by value, then a copy of variable a’s value is used by the
changeVariable() method. The changeVariable() method never gets access to the original variable a in the calling method. It has a
different variable, also named a. Therefore, the calling method prints the value 7.
On the other hand, if a is passed by reference, then the changeVariable() method does not get a copy of the variable named a. It gets
the real thing. The name a in the calling method and the name a in the changeVariable() method refer to the same variable.
Therefore, System.out.println() prints 10.
Note that the names aren’t important here. If changeVariable() were written using i or some name other than a for its argument, the
result would be the same. What makes the difference is whether the variable is passed by reference or by value. If a variable is
passed by reference, it can change in the calling method. If it’s passed by value, it cannot. Java passes all arguments by value, not by
reference.
Because object and array variables in Java are references to the object or array, it can appear as if an object is passed by reference if
you modify only the fields of the object or array, but do not change the reference itself. For example, consider this program:
import java.awt.Point;
class changePoint {
p.x = 38;
p.y = 97;
}
java.awt.Point[x=38,y=97]
The point has therefore been changed. However, the reference, which is what was really passed, has not been changed. To see that,
consider the following program:
import java.awt.Point;
class dontChangePoint {
java.awt.Point[x=0,y=0]
The issue of whether Java really has pointers seems to generate countless flame wars on Usenet, almost as many as are generated
by Star Trek trivia. Some of this confusion is a result of incorrect or incomplete knowledge. Even more of the confusion is a result
of using the same word to mean two different things.
In this book, I use the word pointer to mean the address of a byte of memory on the computer. This is the definition of pointer
used by assembly language, C, and C++ programmers. Java has no equivalent for this kind of pointer.
Some programmers, particularly those accustomed to pointerless languages like Fortran 77 and Basic, use the word pointer in a
very abstract sense, in which it is just about anything that gives a reference to or points to a block of data. Thus, an array that
contains indexes of entries in another array is often said to be an array of pointers, although C programmers would not recognize
it as such. In this sense, Java does have pointers.
Some programmers also claim that Java has pointers because the virtual machine may use pointers in the same sense as used by
assembly language programmers to implement Java’s references. In fact, some implementations of Java, particularly Microsoft’s,
do use pointers in exactly this fashion. However, the .class file verifier severely restricts what you can do with these pointers. In
particular, you cannot use them as freely as you can in C or assembly language. You cannot perform arithmetic on them. You
cannot convert them to and from numeric data types like int. Furthermore, the virtual machine need not implement references as
pointers. Sun’s virtual machines use handles instead. Others may use something completely different, like numeric indexes into a
large, static array.
The bottom line is that Java doesn’t have pointers in the sense that 95 percent of the people who talk about pointers mean. It’s best
just to use the term reference, and try to stay out of flame wars when possible.
In this example, a copy of the reference p1 was passed to the dontChangePoint() method. A new Point object was then assigned to
that copy. This, however, did not change the old reference in the main() method. In the previous example, the reference p in the
changePoint() method and p1 in the main() method both referred to the same object. In this example, p and p1 refer to different
objects after the new Point is assigned to p.
Special references
There are three special references in Java source code: null, this, and super. The meaning of these references generally depends on
their context.
The null reference is an invalid reference. It has no type, and thus may be assigned to a variable of any reference type. When a
reference variable is declared but not constructed, it is initially equal to null.
The special reference this always refers to the current object. For example, the statement
int j = this.x;
sets the variable j equal to the x field of this object. Using the this reference is normally optional. Using “int j = x” would work
equally well. However, on occasion, a variable declared inside a method can shadow a field. This is most common in constructors.
For example, consider this elaboration of the 3Dpoint class:
double x;
double y;
double z;
this.x = x;
this.y = y;
this.z = z;
// other methods...
The three arguments to the constructor — x, y, and z — shadow the fields of the same name. Inside the 3DPoint constructor, x, y,
and z no longer refer to the fields of the object but rather to the arguments to the method. However, this.x, this.y, and this.z still refer
to the fields x, y, and z.
The this keyword can also be used to call a different constructor in the current class. With this usage, this is not, strictly speaking, a
reference. The this keyword can be used this way only in the first statement of another constructor. For example, to call the 3DPoint
(double x, double y, double z) constructor from the noargs 3DPoint() constructor, you would write
public 3DPoint () {
this(0, 0, 0);
This technique is especially common in polymorphic constructors. Arguments not passed to one constructor are filled in with default
values as a call to another constructor.
For example, the java.awt.Component class has a handleEvent() method, so its subclasses do, too. Specifically, java.awt.Container
has a handleEvent() method; java.awt.Frame() has a handleEvent() method; and any subclass of java.awt.Frame that you write has a
handleEvent() method. Now let’s suppose that you want to write a subclass of Frame that allows the window (that is, the Frame) to
be closed. One way to do this is to override handleEvent() in your subclass of Frame so that it handles WINDOW_DESTROY
events. That method might look like this:
if (e.id == Event.WINDOW_DESTROY) {
hide();
return true;
}
else return false;
This method will close the window (by calling the Frame’s hide() method), but it doesn’t handle any other events. It completely
misses mouse clicks, key presses, action events, and more. This would normally be handled by the handleEvent() method of the
Component class, but we’ve shadowed that method with our own handleEvent(). Once we’ve finished our custom processing of the
WINDOW_DESTROY event, we want to pass all other events to the handleEvent() method of java.awt.Component. The super
keyword acts like a reference to that class that lets us do that. Instead of writing “else return false;”, write “else return super.
handleEvent(e);”. This calls the handleEvent() method in the superclass rather than the handleEvent() method in this class. Here’s the
complete method:
if (e.id == Event.WINDOW_DESTROY) {
hide();
return true;
}
else return super.handleEvent(e);
Using the super keyword like this finds the nearest method with a matching signature. In this case, it’s the handleEvent() method in
java.awt.Component. If there were a handleEvent() method in java.awt.Frame or java.awt.Container, that would be called instead.
Like the this keyword, super also has a non-reference meaning inside a constructor. If you use super() as the first statement in a
constructor, it calls the matching constructor in the immediate superclass.
If you do not include an explicit call to super() as the first statement in your constructor, then the compiler will insert such a call into
the byte code. The compiler always chooses the noargs super() constructor if you don’t explicitly choose a different one. This can
lead to annoying bugs that prevent you from instantiating a subclass if the superclass doesn’t have a public or protected noargs
constructor. For example, consider this incorrect Java program:
public superclass(int i) {
If you try to compile this program, you get the error message “No constructor matching superclass() found in class superclass:
superclass.java line 12”. This is a common problem for novices. What you should do to fix this is completely un-obvious, because
you never actually called the superclass() constructor that you’re being warned doesn’t exist. The solution is either add a noargs
constructor to the superclass or to call the superclass(int i) constructor in the first line of the subclass. For example,
subclass() {
super(0);
}
Introductory texts and classes about object-oriented programming spend a lot of time trying to explain the difference between objects
and classes. If there is a single thing that separates people who understand object-oriented programming from the people who merely
know a few buzzwords, it’s the proper use of the words object and class. It’s drilled into students that they are two different things,
as different as the recipe for a cake and the cake itself. However, now that you are a more advanced student of object-oriented
programming, I can tell you the truth. Classes really are objects, at least in Java.
A Java .class file contains the byte code for a particular class. When the Java VM loads a class, a ClassLoader object reads the byte
codes and uses them to instantiate a new object of type java.lang.Class — in other words, a Class object. A Class object has methods
that are useful for deducing information about the class at runtime.
There are two primary ways that your program can bootstrap a reference to a Class object: a ClassLoader object can load the class
from bytes, or your program can call the static method Class.forName(String s) to load the class given its name. For example:
The name of the class must be the fully qualified name, including the entire package. For example, you must write java.lang.Thread
and not just Thread. This is true regardless of the import statements in the program or whether the class is in the java.lang package.
Once you have a Class object, you can use the newInstance() method to create instances of the class. Its signature is
For example:
try {
Object o = threadClass.newInstance();
}
catch (InstantiationException e) {
}
catch (IllegalAccessException e) {
}
The object is returned without any type information, though. In other words, it’s a raw java.lang.Object, not a java.lang.Thread. You
can cast the created object to the appropriate type like this:
Thread t = (Thread) o;
You often see these three steps combined on one line like this:
if (o instanceof Thread) {
Thread t = (Thread) o;
// ... work with the Thread
}
else if (o instanceof Applet) {
Applet a = (Applet) o;
// work with the Applet
}
In general, though, it’s much easier if you know your objects’ types at compile time.
Aside from the type change, the newInstance() method behaves exactly like using the new operator with the noargs constructor for
the class. The following lines produce identical byte code:
If a class doesn’t have a noargs constructor, you can’t instantiate it with the newInstance() method. For example java.lang.Integer has
two constructors:
public Integer()
If you try this, a java.lang.NoSuchMethodError will be thrown at runtime. Integers and other classes that do not have noargs
constructors must be instantiated with the new operator.
Once you have a Class object, there are several other methods to help you determine runtime type information.
The getName() method returns the full package and class name of the class of this object. Its signature is
The name returned is always the most specific type possible, never a superclass or an interface.
The getSuperclass() method returns a Class object representing the class of the immediate superclass of this object. Its signature is
public Class getSuperclass()
You can use this to walk the class hierarchy of an object. For example, given a Class object c,
However, java.lang.Object does not have a superclass, so if the Class object is of type java.lang.Object, then null is returned. Thus,
the last name printed by this loop will always be java.lang.Object because that’s the ultimate superclass of all Java objects. Also, null
is returned if the object is an interface.
Interfaces are also loaded into the VM as objects of type Class. You can test whether a Class object in fact represents an interface
with the isInterface() method. It returns true if the Class object in question represents an interface and false if it doesn’t. Its signature
is
The getInterfaces() method returns an array containing Class objects that represent interfaces. Its signature is
If the Class object represents a class, then the array contains Class objects representing all interfaces implemented by the class.
However, if the Class object represents an interface, then the array contains objects representing all the interfaces extended by this
interface. This array may be of length zero if the Class object neither implements nor extends any interfaces.
The one remaining piece of information about a Class object is the ClassLoader that was used to load the class from bytes on disk or
on the network. The getClassLoader() method returns the ClassLoader object which loaded this class. It returns null if the class was
not created by a ClassLoader. Its signature is
Finally, there’s the usual toString() method for creating a string representation of the Class object. The string is the word class or
interface followed by the full, package-qualified name of the class. Some examples are “class java.lang.Integer” and “interface java.
io.Serializable.”
Listing 3-1 is a program that demonstrates most of these methods. The static printRTTI(Object o) method is the heart of it. This
method checks all the possible type information about the object that it has passed and prints it on System.out. This can be very
useful for debugging. The main() method tests the program on two objects: an Integer and a DataOutputStream. The three methods
printHierarchy(), printInterfaces(), and printClassLoader() break up the code to make it a little more legible. Each one handles a
particular aspect of the runtime type.
import java.io.*;
if (o == null) {
System.out.println("This object is null");
return;
}
Class c = o.getClass();
printHierarchy(c);
printInterfaces(c);
printClassLoader(c);
Class[] ci = c.getInterfaces();
if (ci.length > 0) {
if (c.isInterface()) {
for (int i = 0; i < ci.length; i++) {
System.out.println("extends" + ci[i].getName());
}
}
else {
System.out.println("implements ");
for (int i = 0; i < ci.length; i++) {
System.out.println(ci[i].getName() + ",");
}
}
}
ClassLoader cl = c.getClassLoader();
if (cl != null) System.out.println("This object was
loaded by " + cl);
}
}
Now that you know that classes are objects, I’ll confuse you a little more by telling you that objects are classes, too. As with the
Class class, there is, however, a difference between the big O Object (which is a class) and the little o object (which is an object).
The java.lang.Object class is the common superclass for all Java classes. All classes eventually extend java.lang.Object. Thus, all
classes have access to the methods of java.lang.Object. The primary purpose of java.lang.Object is to provide several useful methods
that the programmer can count on all classes having. These are clone(), equals(), finalize(), getClass(), hashCode(), notify(), notifyAll
(), toString(), and wait(). Furthermore there’s a single constructor, Object(), but you’ll rarely (if ever) call it directly.
What these methods have in common is that they represent internal details of Java objects, not anything external to Java like a
window, a mouse, a motorcycle, a supernova, or anything else that exists outside the virtual machine. Objects normally represent real
world entities. However, the java.lang.Object class is something of a meta-class — that is, a class that represents objects. The clone()
method copies objects. The equals() method compares objects. The getClass() method returns the class of an object. The hashCode()
method computes a unique integer for an object. The toString() method creates a string representation of an object. The notify(),
notifyAll(), and the wait() methods interface between objects and threads. What all these methods have in common is that they treat
objects as computer-based abstractions, not as real world things. Let’s take a closer look at these methods.
Cloning
The clone() method makes a bitwise copy of an object in memory and returns the copy. In other words, it creates a new instance of
the object’s class and copies the values of each field of the object into the new object. It copies all fields, whether they’re public,
private, protected, or package protected.
Not all objects can be cloned. In fact, by default an object may not be cloned. Only objects that implement the java.lang.Cloneable
interface may be cloned. The Cloneable interface does not actually declare any methods. It just tells the clone method in java.lang.
Object that it’s okay to clone this object. If you try to clone an object that does not implement the Cloneable interface, a
CloneNotSupportedException is thrown.
Equality
The equals() method compares two objects for equality. Because equals() is a method of java.lang.Object, any object can be
compared for equality to any other object. That is, every object at least inherits an equals() method.
The java.lang.Object.equals() method just checks to see if two reference variables refer to the same object. For example, consider the
following three reference variables:
Using the equals() method in java.lang.Object, i1 is not equal to i2 because i1 and i2 refer to two different objects, even though those
objects have the same value. On the other hand, i2 and i3 are considered equal to each other because they refer to the same object.
This is often not the behavior you want. Therefore, most classes override equals() with a method that is more appropriate to the
specific class. For example, the equals() method in java.lang.Integer does in fact test the values of the Integer objects to see if they’re
the same. Thus, i1.equals(i2) returns true because the equals() method in java.lang.Integer behaves differently than the equals()
method in java.lang.Object.
It’s nonetheless important to realize that there are often multiple, sensible ways to decide whether two objects are equal. For
example, consider these two URL objects:
www.inch.com and worm.inch.com are different names for the same machine, so these URLs point to the same page. But are the
URLs the same? Maybe not. After all, one of the host names could be moved to a different machine while the other one stayed
behind. Or consider these two URLs:
These two URLs point to the same page on the Web. However the first URL goes over a 100 Megabit per second (Mbps) FDDI
connection and the second over a 10 Mbps Ethernet connection. So these URLs are probably best considered to be unequal.
In fact, Java considers all four of the URLs above to be unequal. The equals() method in the URL class, like most of the equals()
methods in the java packages, is relatively shallow. It does not attempt to discover whether two different objects might mean the
same thing if they are superficially different.
This isn’t all. What about these two URLs?
These point to different sections of the same page. Although they are different in one sense, Java considers them to be equal because
they point to the same page.
The bottom line is that there are few guarantees about how the equals() method behaves. The only thing you can be reasonably
confident about is that references to the same object will be equal to each other. Otherwise, you have to do the best you can.
Regrettably, the documentation for the equals() methods is often incomplete. The only real way to find out what a particular equals()
method really does is to look at the source code. If that’s not possible, equals() methods tend to be simple enough to understand from
decompiled or disassembled byte code. If for some reason you can’t disassemble or decompile a class, then you have to run as many
tests as you can think of to determine what’s really going on.
Finalization
The finalize() method is called when an object is garbage-collected. The finalize()method is the programmer’s last chance to do
something with an object. The finalize() method of the java.lang.Object class is an empty method; that is, it does absolutely nothing.
It looks something like this:
You may ask yourself, why even have a finalize() method in java.lang.Object if it never does anything? The reason is so that the
runtime knows that it can always call an object’s finalize() method. A method doesn’t have to do anything to be called. Subclasses of
java.lang.Object may override finalize(). If they do, their finalize() method is called. Otherwise the finalize() method in java.lang.
Object is called. It’s much simpler to know you can always call finalize() for any object at all than to have to check whether an object
has a finalize() method before calling it.
In Java, an object can tell you what class it belongs to via the getClass() method of java.lang.Object, which has the following
signature:
getClass() returns a Class object (an object of type Class) that can be manipulated with the methods of the last section.
Hash codes
Hash codes are integers used as keys in hash tables. Each object that can serve as a key in a hash table must be associated with a
precise integer. The list of items in the hash table is then indexed with these integer keys. Equal objects — objects which compare
equal to each other with the equals() method — are supposed to have identical hash codes. Unequal objects normally have different
hash codes, although this is not always true. The efficiency of a hash table is closely related to the percentage of objects in the table
with unique keys.
The default hashCode() method used by java.lang.Object is the numeric value of the reference to the object. Although Java programs
aren’t allowed to convert 32-bit references to 32-bit ints, you can do this in native C code, and that’s exactly what Java does, at least
on 32-bit platforms. (Porting Java to 64-bit platforms like the DEC Alpha or 16-bit platforms like Windows 3.1 is decidedly non-
trivial, for this and many other similar reasons.)
No reference can point to two different objects. Therefore, hash codes calculated in this fashion will always be unique. Conversely,
no object can have two different addresses, so an object always has the same hash code. (An object can be referred to by two
different reference variables, but these two variables will still have the same value.)
The hashCode() method is closely tied to the equals() method. When you override equals(), you need to override hashCode(), too.
Remember that all objects that are equal according to the equals() method must have the same hash code.
Threading
Discussion among many people in the same place at the same time tends to degenerate rapidly into babble as everyone begins talking
at once. To make discussion possible among large groups of people, a special object, sometimes called a “magic feather,” is created
and endowed with the special power that only the person holding the magic feather may speak. Because no more than one person can
hold the magic feather at a time, no more than one person can talk at one time.
Many different threads talking at the same time can be a huge problem for Java programs as well. It is extremely important to
guarantee that two different threads don’t modify the same object at the same time. Therefore, each object is created with its own
magic feather. The magic feather for an object can be held by at most one thread at any given time. As long as a thread holds an
object’s magic feather, it can do anything it’s normally allowed to do with the object. All other threads that want to use the object
have to wait until they get the object’s magic feather.
I should note that “magic feather” isn’t a sufficiently impressive technical term for most programmers. Instead, the commonly used
word is monitor.
If you search the java packages, you won’t find any class called monitor or magic feather. A monitor is not a separate object. It is a
part of each individual object. Threads ask for an object’s monitor when they execute a synchronized instance method of the object,
execute the body of a synchronized statement that synchronizes on the object, or invoke a synchronized static method of a class. (In
the latter case, the Class object associated with the class is synchronized.) Threads give back the monitor when they finish executing
the synchronized code.
By calling one of an object’s wait() methods, a thread can yield possession of the monitor and put itself to sleep until the monitor is
available again. The thread can then be awakened with the object’s notify() or notifyAll() methods. There are three polymorphic wait
() methods. These are
public final void wait() throws InterruptedException
public final void wait(long ms) throws InterruptedException
public final void wait(long ms, int ns) throws InterruptedException
Each of these methods causes the calling thread to release the object’s monitor and go to sleep until another thread notifies threads
waiting on this object’s monitor to wake up. At that point, the thread wakes up, waits until it can regain the object’s monitor, and
then resumes running. The first wait() method, with no arguments, sleeps indefinitely. The second, with a single long argument,
sleeps for at most the specified number of milliseconds and then wakes up whether it has been notified or not.
The third and final wait() method allows more finely grained control, down to a nanosecond. The first argument is the number of
milliseconds to wait before waking, and the second argument is the number of nanoseconds to add to that. Not all architectures allow
such finely grained timing. You shouldn’t rely on accuracy of more than a millsecond or two.
There are two notify methods: notify() and notifyAll(). Their signatures are
The notify() method wakes up a single thread that’s waiting on this object’s monitor. The notifyAll() method wakes up all threads
waiting on this object’s monitor. Both of these methods should be called only in a thread that owns the object’s monitor.
Strings
The final thing that all objects must be able to do is to provide a string representation of themselves. They can do this with the
toString() method. The default toString() method from the java.lang.Object class merely prints the name of the class of the object.
Most classes will override this method with one that provides more information.
The toString() method is rarely called explicitly. It is instead invoked implicitly when an object is passed as an argument into a print
() method or is concatenated with a string using a + sign. I’ll talk more about strings and toString() methods later in this chapter.
Arrays
What is an array? To a high-level programmer, an array is an indexed list of values of the same type with a fixed length. The most
important feature of an array is that you can retrieve any particular element of the array in constant time. In other words, it takes no
more or less time to retrieve the seventh component of the array than it takes to retrieve the 70th or the 700th.
Secret: Internally, arrays are contiguous blocks of memory in the heap. To find the size of an array, multiply the size of
the array’s type by the length of the array. For example, an int[] array with length 60 takes up 240 bytes of heap memory
because it has space for 60 four-byte ints. The memory is used even if the space isn’t occupied.
Primitive data types like shorts and ints are stored directly in the array. They take up no more space than the array itself. In fact, an
array of shorts, bytes, or chars takes up less space than the same number of short, byte, or char variables. Individual variables of
these types are always promoted to ints. Thus, a byte variable occupies four bytes, and four byte variables occupy 16 bytes because
each byte is promoted to an int. However, promotion does not occur inside arrays, so an array of four bytes occupies exactly four
bytes. Similarly, an array of four shorts or an array of four chars occupies exactly eight bytes.
Arrays of objects are a little different. Each entry in the array is not the object itself but rather a reference to the object. Each
reference requires four bytes, whether or not that reference is null. However, if a component of the object array is indeed non-null,
then somewhere else in the heap is an object that also needs memory. When you calculate the total memory needed for an array of
objects, you have to account for the array and the objects themselves separately. For example, consider this array:
By looking at the source code for the Integer class, you discover that each Integer object has a single non-static int field. Thus, each
Integer object occupies four bytes of heap memory. When the Integer[] array iarray is created, it has space for ten references. This
takes up 40 bytes. As new Integer objects are created and added to the array in the for loop, each of these takes an additional four
bytes. When the loop is complete, the array occupies 80 bytes of the heap. If you then set some of the array components to null, the
corresponding Integer objects would eventually be garbage-collected, and the array would shrink to somewhere between 40 and 80
bytes of the heap.
Whether the array ever really occupies more than 40 bytes is a semantic question. You could just as reasonably say that the array
always occupies 40 bytes and additional space may be occupied by the objects to which the array’s components refer. As long as you
understand what’s really going on, you can say it however you want.
Multidimensional arrays
Java does not have true multidimensional arrays like Fortran does. Instead, it fakes them with one-dimensional arrays of references
to one-dimensional arrays, much in the fashion that C does. For example, consider a two-dimensional array of doubles like this:
This is allocated in two parts. First, a one-dimensional, length four array of references is allocated; then, each of these is pointed at a
one-dimensional, length three array of doubles. In other words:
Note: As you’ll see in Chapter 5, Java can accomplish this with one byte code instruction that has the same effect as the
above code.
This means that even though matrix is declared as a two-dimensional array of doubles, matrix[0], matrix[1], matrix[2], and matrix[3]
are all legitimate Java entities. For example, you can copy the first row of matrix into the third row using System.arraycopy() like
this:
You can use matrix[0], matrix[1], matrix[2], and matrix[3] anywhere a one-dimensional array of doubles is expected. This also
means you can create ragged arrays — that is, arrays that do not have a fixed length in one dimension. For example,
The zeroth row of the triangle array has length one, the first row has length two, the second row has length three, and so on.
Higher dimensional arrays just have additional levels of indirection. For example,
Datacube[0] through Datacube[3] are references to two-dimensional arrays of doubles; more precisely, they’re references to arrays of
references to one-dimensional arrays of doubles. Datacube [0][2] is a reference to a one-dimensional array of doubles and can be
used anywhere a one-dimensional array of doubles is needed.
Arrays are objects. They extend the java.lang.Object class, and you can call toString(), equals(), hashCode(), wait(), notify(), and all
the other methods of the object class on a reference variable of array type. An array can be assigned to variables of type Object and
passed to methods that expect a reference to an Object type. Arrays live in the heap like all other objects do; therefore, you use
reference variables to refer to arrays.
However, there is no java.lang.Array class. Each array has an implicit type of the most primitive type it holds, followed by a number
of left-bracket signs ([) equal to its dimension. Thus, an int[][] has type int[[ and a String[] array has type String[. These are not legal
Java identifiers, and you won’t find them in Java source code, but you will find them in Java byte code.
As well as the methods inherited from java.lang.Object, arrays have a single field called length. This field contains the length of the
array. I used this field above in the line
Other than this field, arrays mostly inherit the methods of java.lang.Object. The one they override is clone(). Arrays implicitly
implement Cloneable, so you can call clone() without getting a CloneNotSupportException. However, the clone of a
multidimensional array is a shallow copy. Only the initial references to the sub-arrays are copied, not the sub-arrays themselves.
System.arraycopy()
The System class contains one important method for working with arrays:
The System.arraycopy() method copies length values from the source array into the destination array. This is used in the StringBuffer
and Vector classes as well as many other places. The basic algorithm looks like this:
In other words, the length components of src_array starting with component src_index are copied in order into the length components
of dst_array starting at dst_index. The arrays src_array and dst_array can be the same array. If so, the copy is made as if the source
components were first copied into a temporary array and then copied back into the array. This allows overlapping copies to work. If
the arrays have reference types, then the copy is a shallow copy. That is, only the references are copied. The objects to which the
references point are not copied.
If the arrays to be copied contain primitive data types, they must contain the same primitive data type. For example, an array of
shorts can only be copied to another array of shorts, not to an array of ints or longs. This is one of the few places in Java where
arithmetic promotion does not take place. If you try to copy an array of one primitive type such as short to an array of a wider
primitive type such as int, then an ArrayStoreException is thrown and the destination array is left unchanged. Similarly if you try to
copy an array of a reference type to an array of primitive type or vice versa, an ArrayStore-Exception is thrown, and the destination
array is left unchanged.
Copies between arrays of reference types are a little more complex. As long as the reference type in one array can be converted to the
reference type of the other array, the copy takes place. Thus, it is acceptable to copy a Float[] array to a Number[] array or an Object
[] array to a String[] array because Floats can be cast to Numbers and Objects can be cast to Strings. (The latter cast works only if the
Objects are in fact Strings. Otherwise, an ArrayStoreException will be thrown.) However if the reference types are incompatible —
for example, Float and String — then an ArrayStoreException will be thrown.
It’s possible for some of the components of the source array to be compatible with the type of the destination array and some to be
incompatible. For example, if src_array has type Object[], then it can contain both Floats and Strings. If dst_array has type String,
then you can copy the Strings from src_array to dst_array but not the Floats. In this case, the components of the src_array that can be
copied starting with component 0 are copied. However, as soon as an incompatible component is encountered, an
ArrayStoreException is thrown, and no further components are copied, compatible or incompatible.
This method can also throw an ArrayIndexOutOfBoundsException, in which case the destination array is not changed. (The source
array is never changed.) An ArrayIndexOutOfBoundsException is thrown if srcIndex, dstIndex, or length is negative, if srcIndex
+length is greater than srcArray.length, or if dstIndex+length is greater than dst.length.
The System.arraycopy() method is normally implemented in native code for maximum performance. While it would be possible to
write an equivalent routine in pure Java, most architectures have extremely efficient native instructions for copying large blocks of
memory from one place to another. Because an array is natively a large block of memory, this is one of the places where native code
helps a lot.
Strings
Strings are objects like any other object in Java. There is a java.lang.String class. However, the compiler has special support for a
number of things you might want to do with strings. For example, you can create a new String object like this:
although you could. You can't do that with any other kind of object. For example, if you write
Double d = 7.5;
Strings are the only class that can be initialized from literals without an explicit constructor call. In point of fact, what’s really
happening is that the compiler figures out that it needs to call a String constructor. It translates the statement “String s=”Hello world!
“;” into the following code:
This is not the only way the String s="Hello world! "; statement could be compiled. However, somewhere there has to be a call to a
String() constructor, even if you didn’t write one in the source code.
Note: Actually, depending on what use is made later of the String objects, an optimizing compiler may include only the
String literal in the byte code and not actually create an object.
The Java compiler handles many other idioms for manipulating strings. For example, you probably know that you can concatenate
strings with a plus sign (+) like this:
String s3 = s2 + s1;
Everything is accomplished with method calls. There are no additive operators that truly operate on strings. That’s an illusion
supported by the compiler. Several other illusions are also supported by the compiler. Consider this common statement:
Here you have a String literal concatenated with an int variable. The compiler will translate this into something like this:
It is much simpler to just write System.out.println("Count is: " + i); than to put all the pieces together inside a StringBuffer yourself.
That’s why this intelligence was built into the compiler and the language specification. Much more complex concatenations are
compiled similarly.
String implementation
In Java, at least the version of Java written by Sun, Strings are implemented as arrays of chars. The String class has three non-static
fields: value (an array of chars), offset (an int), and count (also an int). Every string therefore takes up at least 12 bytes of heap
memory. However, you also need space for the char array.
The char array value has one space for each character in the string. In theory, using the offset and length fields, the value array can
have slots for more chars than are actually present in the string. The offset field says which part of the string contains the first
character of the string, and the length field says how many characters there are in the string. In practice, there’s no way for that to
happen. The value array for the string “Hello world!” thus has length 12.
Strings are immutable. Once a string is created, it is never changed. A String variable may change, but a String object never changes.
Consider the following code fragment:
String s;
s = "Hello World!";
s = "Goodbye World";
Here the String variable s is first null. It then refers to an object with the value “Hello World!” and then to a String object with the
value “Goodbye World”. However, these are two different objects, not different versions of the same object.
Similarly, if you were to concatenate another string to s, it would require the creation of still another String object. For example,
String s;
s = "Hello World!";
s += " Isn't it a beautiful morning?";
String s;
s = "Hello World!";
System.err.println(s);
s += " Isn't it a beautiful morning?";
Three separate strings are created while this program runs. In the source code, however, it looks like one string is being changed.
This is an illusion supported by the compiler.
StringBuffers
Java strings are immutable; that is, a string’s value may not be changed after the string is constructed. This makes strings very thread-
safe and fairly fast, but also makes them excessively inefficient for many operations. For example, suppose you write
String s = "one";
s += " two";
s += " three";
s += " four";
s += " five";
Compiling these statements with only immutable strings would require the construction of five separate strings: first “one,” then
“one two,” then “one two three,” then “one two three four,” and finally “one two three four five.” There’s no way, using only strings,
to create one string and then append the new parts to it.
A StringBuffer is a string that can be changed. You can add additional characters to the end or the beginning or the middle of a
StringBuffer without creating new objects. On the other hand, because StringBuffers are mutable, they’re not inherently thread-safe,
and thus many of the methods of the StringBuffer class are synchronized. This can slow down the execution of a program when
Strings aren’t changing.
In short, the String class is optimized for strings that don’t change; the StringBuffer class is optimized for strings that do change. In
general, the Java compiler is fairly smart about figuring out which class to use where. Nonetheless, the more manipulation of a string
you’re doing, the more efficient it becomes to use a StringBuffer and convert it to a string only when you’re done.
The main methods of the StringBuffer class are append() and insert(). These methods are polymorphic, so they can accept any type
or class of data. The append() method adds characters at the end of the StringBuffer; insert() places the new characters at a specified
position in the StringBuffer. These methods are sufficiently polymorphic to handle all Java data types. There are ten different append
()methods:
Each of these methods converts its argument to a string format and appends it to the StringBuffer. The first six of these methods
handles the primitive data types. Shorts and bytes are promoted to ints before conversion. Strings are also appended directly. All
other object types are converted to a String object, using their toString() method, and then appended. The final append() method
appends an array of chars to the StringBuffer. The second-to-last method appends the entire array, while the last method appends
only the length characters in the array beginning with the character at offset. The only thing that you cannot append to a StringBuffer
is an array of type other than char[].
The insert() methods are almost equally polymorphic with the exception of the methods to handle arrays of chars. These methods are
These methods insert the string representation of their second argument beginning at the offset specified in the first argument. All
characters after offset are shifted to the right. How far they’re shifted is completely dependent on the length of the string format of
the second argument.
It helps to keep this array in mind when considering where to insert an item. Because a StringBuffer is an array, the first character of
the StringBuffer is number zero, not number one.
Also like with Strings, the length of the value array is set when the StringBuffer object is first constructed. There are three
constructors:
public StringBuffer()
public StringBuffer(int length)
public StringBuffer(String s)
The noargs constructor starts with an array of length 16. The second constructor initializes the array to the specified length. The third
constructor starts with an array of length s.length() + 16 — that is, the length of the string plus 16 empty spaces. This is because of
the expectation that whatever length of the string that the StringBuffer initially holds, it will be expanded later.
So far this is very much like the String class. The difference, however, is that the buffer can expand as necessary to hold more
characters. Suppose you have the following code:
The string “Hello world!” has 12 characters. Because 12 characters is six too many for the six character StringBuffer sb, Java
expands the array with the ensureCapacity() method:
The ensureCapacity() method expands the array to two times the current capacity plus one (2 * (capacity() + 1)) or to the requested
minimum capacity, whichever is greater. A new array is allocated of the appropriate size; the old value array is copied into the new
value array with the System.arrayCopy()method; and the reference is set to the new array. In code, it looks something like this:
char[] value;
...
char[] newValue = new char[Math.max(minimumCapacity,
2*(value.length + 1))];
System.arraycopy(value, 0, newValue, 0, value.length);
value = newValue;
Previous Table of Contents Next
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
Notice that the length of the value array is at least doubled. It is not nearly expanded to the minimum necessary length. Although you
don’t want to waste space unnecessarily, it’s even more important not to waste CPU cycles by growing the array every time you have
to add a character to it. You will see this scheme for growing arrays again very shortly. The java.util.Vector class does almost exactly
the same thing.
Note: Although ensureCapacity() is public, you rarely need to call it directly. The insert() and append() methods of the
StringBuffer class will call it when they need to.
The value array is also expanded when an item is inserted into the middle of a StringBuffer with one of the insert() methods.
However, the value array will not be expanded to provide additional space if you try to insert an item past the end of the
StringBuffer. If you try, a StringIndexOutOf BoundsException will be thrown.
There are a few other useful methods in the StringBuffer class. The charAt(int i) method returns the ith char in the StringBuffer. This
is easy to do because you can just return the ith char in the value array.
The setCharAt(int i, char c) method changes the character at index i in the StringBuffer to the char c. This differs from the insert(int
i, char c)method because it actually replaces the character at i rather than shifting it and all following characters to the right. A
StringIndexOutOfBounds Exception is thrown if i equals or exceeds the length of the string.
The length() method returns an int giving the number of characters currently present in the StringBuffer. This is generally not the
same as the length of the value array. That number is called the StringBuffer’s capacity and is accessed with the capacity() method.
The ensureCapacity() method already discussed changes a StringBuffer’s capacity. This is not the same as a StringBuffer’s length.
The capacity of a StringBuffer is the number of characters that can be stored in it without taking time to expand the internal value
array. The length of a StringBuffer is the number of characters currently stored in the internal value array. To change a
StringBuffer’s length, invoke its setLength(int i) method. This does one of two things: if i is less than the current length of the
StringBuffer, then the StringBuffer is truncated to the length i; however, if i is greater than the current length of the StringBuffer,
then it is expanded with null characters to the requested length.
The reverse() method reverses the characters in the StringBuffer in place. For example, if sb contains the string “dam”, after calling
sb.reverse() it will contain the string “mad”.
The getChars() method copies characters from the StringBuffer into an array. Its signature is
A substring of characters from the StringBuffer is copied into the char array dst. The substring to be copied is delineated on the left
by the index srcBegin and on the right by srcEnd-1. The characters of this substring are copied into the subsection of the char array
dst beginning at dstBegin. A StringIndexOutOfBoundsException is thrown if either srcBegin or srcEnd is less than zero or greater
than or equal to the length of the string. An ArrayIndexOutOfBoundsException is thrown if dstBegin is less than zero or greater than
or equal to the length of the array or if the substring exceeds the bounds of the array.
Finally, like most other classes, StringBuffer contains a toString() method. This method is often invoked as the last step in a long
sequence of string operations.
The java.util class includes several abstract data structures to hold collections of objects. On one level, the programmer is supposed
to be shielded from the internal workings of these classes. The whole point of a class library and abstract container classes is to
shield the programmer from low-level details.
However, there are performance issues that can be addressed only by learning how a class is implemented. For example, if the
Vector class is implemented as a linked list, then insertions into the list will be very fast. However, finding a particular element in the
list will be rather slow. On the other hand, if the Vector class is really an array, then insertions into the middle of the list may be quite
slow but retrieving an element from the list can be quite fast.
If you’re writing an applet or application for public distribution, you may not wish to rely on what I say here about the internals of
these classes. Although these data structures are implemented similarly on all Java implementations to which I have access, it is
possible (though unlikely) that the implementation details will change in the future. If the performance of your program depends on a
specific implementation of a class, then you may wish to copy the source code for the class, change its name, and recompile it. Then
you would use your modified class instead of the original. Of course, this will increase the size of the program you have to distribute.
This could be important for an applet, but is unlikely to be significant for an application. As with many other things, you must make
the tradeoffs that are appropriate for your situation. You have to decide what’s more important to you, guaranteed performance
characteristics or smaller download size.
Vectors
A Vector is Java’s basic list class. A list is an ordered collection of items. The fundamental operations on a list are
The main difference between a list and an array is that a list generally doesn’t have a fixed size. It can grow or shrink as needed.
Furthermore, a list doesn’t have empty spaces. If you remove a component from the middle of an array, you leave an empty space.
All the other components stay where they are. On the other hand, if you remove an item from a list, all the items above it in the list
are moved down to fill the space left.
Implementation
There are many different data structures that you can use to implement these operations. The very word list suggests a linked list data
structure to many programmers. However, the Vector class is not a linked list, although you can do with a Vector anything you can
do with a linked list. Instead, a Vector is a growable array.
If that sounds funny to you, it should. Java arrays are not growable. You cannot take an array that was initialized with space for ten
doubles and expand it so that it has space for twenty doubles. Once an array is created, its size is fixed.
However, you can, memory permitting, create a new array with space for 20 doubles. Then you can copy the components of the
original array into the new array. If you then adjust all references to the old array to point to the new array instead, it’s as if you grew
the old array.
The hard part of this procedure is finding all the references to the old array. Java solves this problem by making the array you want to
grow a protected field in a public class: Vector. Only the Vector class ever holds any references to the array, and it has only one
reference to the array. Other objects have references only to Vector objects. Therefore, when the array needs to be grown, you need
to adjust only a single reference in the Vector class. This sort of data encapsulation is one of the primary advantages of object-
oriented programming.
You should recall that this is almost exactly what the StringBuffer class does when it needs to expand. StringBuffers use an array of
chars instead of an array of objects like Vectors, but otherwise the logic is the same.
Object[] elementData;
...
// double the vector's capacity
Obj ect[] newData = new Object[2*elementData.length];
System.arraycopy(elementData, 0, newData, 0,
elementData.length);
elementData= newData;
The array that grows is a protected field of the Vector class called elementData; the number of elements currently stored in the array
is a protected int field called elementCount. The elementCount, the number of elements in the array right now, should not be
confused with the capacity of the array, which is the number of elements that can be stored in elementData before you need to grow
it. In other words, the capacity is elementData.length.
The fact that vectors are really just arrays behind the scenes has many implications for the performance and efficiency of the
fundamental list operations. It means that insertions at the end of the list are quick, unless the vector has run out of space, in which
case they can be quite slow. Insertions into the middle of the list are always slow. Finding the item at a given position in the list is
fast.
The array implementation of the Vector class also sets an upper limit on how many items you can place in a Vector. A Vector can
hold up to 2,147,483,647 objects because an array is indexed with a signed int. In practice, you’d run out of memory before you ever
stuffed that many objects into a Vector.
public Vector()
public Vector(int initialCapacity)
public Vector(int initialCapacity, int capacityIncrement);
Every Vector has a capacity — that is, the number of objects it can hold before it must be expanded. Because it takes time to expand
a Vector, you generally want to have some empty space in a Vector so you don’t need to expand it every time you add an element to
it. However you don’t want to waste space if you can avoid it.
The noargs Vector() constructor creates a new Vector with an initial capacity of ten. When that capacity is exceeded, the Vector
doubles in size. That is, when you add the eleventh element to the Vector, it expands itself to a capacity for 20 elements. When you
add the 21st element, the Vector expands itself to a capacity for 40 elements, and so on.
If you know how many elements that the Vector will probably need to hold when you construct it, you can speed up your program by
using the Vector(int initialCapacity) constructor. For example, if you know you’re going to put about 30 elements in a Vector, give
or take five, then you should construct it like this:
Unless the maximum number of elements you’ll place in the Vector is substantially greater than the average number of elements
you’ll put there, you should always construct a Vector with the maximum number of elements you expect. Each empty space in a
Vector only takes up four bytes. Therefore, unless you’re really pressed for space, it’s much more important to avoid unnecessary
resizing than to worry about the space wasted by a few empty slots.
If you don’t want a Vector’s capacity to double every time it needs to be expanded, you can also pass a capacity increment to the
constructor. For example, to create a Vector whose capacity increases in blocks of ten at a time and has an initial capacity of 35, you
would write:
It’s rather unusual to do this, however. Most of the time the disadvantage of the CPU time you’ll lose to the extra resizing outweighs
the small intermediate space savings you’ll achieve.
There are two methods to put a new object into a vector, addElement() and insertElementAt(). Their signatures are
addElement() places the object at the end of the Vector. This method takes essentially constant time as long as there’s empty space
left in the vector. It takes slightly longer if the Vector first needs to be grown.
insertElementAt() places the object at the specified position in the vector. All elements of the vector at or beyond the specified index
are moved up one to make room for the newcomer. The efficient native method System.arraycopy() is used to move the remaining
elements up, but it’s still less than ideal. If a program requires frequent insertions into the middle of a list, you may well be better off
writing your own linked list class.
You can also replace elements in vectors. Because the size of the vector stays the same, this is always quite fast. The relevant method
is:
The object that was previously at index is no longer there. Instead, it is replaced by the new Object o. If there are no other references
to the old object, then it will eventually be garbage-collected.
Elements can also be removed from a vector. This generally requires moving elements down that were above the object. Again this
can be done with the System.arraycopy() method, but if you’re doing a lot of this, you should consider using a linked list instead.
The method is
You can also remove an object from a Vector without knowing its index. The method to do this is
However, this method requires Java to traverse the entire Vector, searching for the requested object. Thus, its execution time is
proportional to the length of the array. In computer science terms, this is an O(n) (pronounced “order en”) operation where n is the
number of elements in the Vector.
Furthermore, only the first occurrence of the object in the Vector is removed. This may or may not be what you expect. Removing all
references to the object from the Vector requires multiple passes through the vector. If the object is found and removed,
removeElement() returns true. Otherwise, it returns false. You can remove all references to an object o from a Vector v in one line of
code like this:
The removeAllElements() method deletes every element in the Vector. Its signature is
In the current implementation, Java deletes all elements from a Vector by looping through the Vector and setting each element to
null. This is an O(n) operation, but it’s less than optimally efficient. It looks something like this:
Other implementations are possible. For example, you could simply reallocate a new elementData array like this:
You could even leave the elementData array untouched and change only the elementCount field. This is even quicker. The old
elementData array components will simply be overwritten as necessary. However, this has the potential to cause memory leaks
because references to the old components still exist in the array, and thus those objects will not be garbage-collected.
In addition to the above methods for manipulating the contents of a Vector, there are several methods to find objects in a Vector. The
elementAt(int i) method returns a reference to the Object at index i in the Vector. If i is not a valid index into the Vector, an
ArrayIndexOutOfBoundsException is thrown.
The firstElement() and lastElement() methods return references to the first and last elements in the Vector respectively. If the Vector
has no elements, and thus no first or last element, a NoSuchElementException is thrown.
Five methods search for a particular object. Because this requires a traversal of the array, these methods are proportional to the
number of elements in the Vector. These methods are:
The contains() method returns true if o is in the Vector and false if it isn’t. The indexOf() and lastIndexOf() methods return the first
and last indices of the specified object in the Vector respectively. If the object is not found in the Vector, then -1 is returned.
You can pass an extra int to these methods to indicate the element at which to begin searching.
Miscellaneous methods
There are three other useful methods in the Vector class. The isEmpty() method returns true if the Vector has no elements and false if
it has one or more elements.
The setSize() method either shrinks or expands the Vector to a given non-negative size. If the vector is expanded, then the new
elements are set to null. If the vector is shrunk, then elements past the requested size are removed from the Vector.
Finally, the elements() method returns a reference to an object which implements the Enumeration interface. This provides a
convenient way for external classes to process each and every element of the Vector. For example,
Enumeration e = v.elements();
while (e.hasMoreElements()) {
Object o = e.nextElement();
// work with o
}
Vectors are one of the most useful container classes in the java.util package. However, they’re not right for everything. In particular,
Vectors are slower than raw arrays; they can’t handle primitive data types, and they can be quite slow when inserting or removing
objects from any place except the end of the Vector.
If you can use a fixed array instead of a Vector, you should. As a rough rule of thumb, any operation that uses a Vector is about three
times slower than the same operation performed with a plain array. That’s primarily a result of the extra method calls needed to
perform basic insertions, deletions, and accesses. It’s always better to do it directly if you can.
The most common reason to use a Vector is that you don’t know how many objects you’ll need to deal with at compile time, only at
runtime. However, if the Vector is going to be filled only once and then not modified, you’re better off using a real array.
For example, one place common place vectors are used unnecessarily is in processing <PARAM> tags passed to an applet. It’s quite
common to pass an undetermined number of parameters to an applet like this:
You may not know when you compile an applet how many of these parameters there will be. At first glance this appears to be a
perfect place for a Vector. You can read these parameters into the Vector in your applet’s init() method like this:
However, this is overkill. You do not need the full power of a Vector just to collect an undetermined number of quantities. There are
several alternative solutions to this problem that don’t involve the overhead of a Vector. For example, you could create the Vector as
above, then copy it into an array with an Enumeration, like this:
Alternatively, you could just use the first loop to determine how many PARAM tags you have to deal with, then use a second loop to
read them, like this:
String name = null;
int i = 0;
while ((name = getParameter("String" + ++i)) != null) {
}
String[] names = new String[i];
for (int j = 0; j <= i; j++) {
names[j] = getParameter("String" + (j+1));
}
There are some other more complicated solutions, such as implementing your own growable array, but the bottom line is that you
shouldn’t use a Vector as merely an array whose size will be determined at runtime. You should use a Vector only when the array is
going to be constantly changing size and having elements removed and deleted. If you’re only calling addElement() and never
insertElementAt(), contains(), removeElement(), or the other such methods of the Vector class, then using a Vector instead of a plain
array will make your program slower than it needs to be.
Bitsets
The Bitset class is an indexed list of bits. Each component of a Bitset has a boolean value: true or false. A one or set bit is considered
to be true and a zero or unset bit is considered to be false. The primary purpose of a Bitset is to provide an extremely space-efficient
means of storing many binary values. Bitsets are not necessarily very fast, but they should be very small.
Java implements Bitsets as arrays of longs. The first 64 bits — that is, bits 0 through 63 — are stored in the zeroth component of the
array. The second 64 bits — that is, bits 64 through 127 — are stored in the first component of the array, and so on.
Extensive manipulation of these longs with the bit-level operators discussed in Chapter 2 is used to extract and set the values of
individual bits. For example, consider the process of extracting the value of bit 97 from the Bitset bs. At the high level, all you need
to do is this:
boolean b = bs.get(97);
Now consider what you have to do to get the value of the 97th bit from an array of longs called la:
boolean b;
long L = la[1]; // the 97th bit is in the first component
L = L & 0x00008000; // mask off the 97th bit
if (L == 0) b = false; // bit 97 wasn't set
else b = true; // bit 97 was set
That’s a lot more complex, which is why this is hidden inside a class in the first place. Of course you want a general method for
finding an arbitrary bit. That takes even more effort, such as this:
long[] la;
Sun’s actual code is a little faster than this, but a little harder to understand.
Some operations are easier. For example, the logical operations and, or, not, and xor simply require performing the equivalent
bitwise operations between each corresponding component of the arrays forming the two Bitsets. The only catch is that you may
need to expand one array if it’s smaller than the other is.
Stack
The Stack class implements a classic stack data structure, that is a last-in-first-out (LIFO) set of objects. There are three fundamental
operations you can perform with a stack:
Notice that all three of these operations operate only on the top of the stack. To see what’s further down in the stack, you must first
remove everything that’s on top of it.
Note: Some people claim that there are only two fundamental stack operations: pushing and popping. Peeking can be
considered to be a pop followed by a push of the object back onto the stack.
Java provides methods to perform all three fundamental operations: pushing, popping, and peeking. They are
When an object is placed in a stack, it loses its type information. Therefore, when you retrieve it from the stack, you have to cast it
back. For example,
Java has two more utility methods that, while useful, are not part of the minimal requirements for a stack. They are
The empty() method returns true if the Stack contains no elements or false if it does not.
The search method returns an int that tells you how deep in the stack the object o is. The object on the top of the stack is at position
0; the second to the top object is at position 1, and so on. If the object is not found in the stack search() returns -1.
Objects are tested for their presence in the stack with the equals() method, not with ==. Thus, in some sense, search() is looking for
an object equivalent to the requested object, not necessarily the same object. For example, consider the following code:
When this code has finished, the stack looks like this:
If you print out the result, you will see that it’s 0, not 3, even though u1 is at position 3 in the stack, not position 0. The stack is
searched from top to bottom for the first object for which o.equals(u1) is true. In this example that’s u2, http://sunsite.unc.edu/
javafaq/javafaq.html, because the URL class’s equals() method does not consider the ref to be part of a URL.
In Java, the Stack class extends the Vector class. Therefore, you can do anything with a stack that you can do with a Vector. Most
significantly this means that you can use the at methods: elementAt(), insertElementAt(), removeElementAt(), and setElementAt().
This is unfortunate because it allows the integrity of a stack to be violated. The whole point of a stack is that operations always take
place only on the top of the stack. Many algorithms depend on this assertion. It is extremely bad style to violate this directive.
It is certainly true that there are situations in which it is useful and convenient to operate on elements in the middle of a list.
However, if this is what you need to do, you should use a raw Vector, not a stack.
The Stack class was written by Jonathan Payne. My best guess is that he decided to implement Stack as a subclass of Vector in order
to save time through code reuse. However, what he probably should have done was to have used the Vector class via encapsulation
rather than inheritance. Listing 3.2 demonstrates how the Stack class could have been quickly written without allowing too much
access to the nether regions of the stack.
/*
* @(#)Stack.java 1.12 96/12/09
*
* Copyright 1996 Elliotte Rusty Harold.
*
* Permission to use, copy, modify, and distribute this software
* and its documentation for all purposes and without
* fee is hereby granted provided that this copyright notice
* appears in all copies.
*/
package com.macfaq.util;
import java.util.*;
/**
* A stack that guarantees its own integrity.
*
* @version 1.0, 12/09/96
* @author Elliotte Rusty Harold
*/
public class Stack {
theStack.addElement(item);
return o;
Object o;
Object o;
int i = theStack.lastIndexOf(o);
This Stack class performs all the necessary stack operations; it’s still based on the Vector class, so it can take advantage of any
optimizations made there, but it does not allow other classes to violate the stack structure.
Summary
This chapter is really about distinctions between concepts and words that are very closely related.
! You learn what Java objects really are: chunks of memory in the heap. You also learn that Java uses references to locate
those areas of memory in the heap and that all access to objects takes place through references. You learn how a reference
to an object differs from the object itself.
! There is a class for objects (java.lang.Object), and classes are themselves objects of type java.lang.Class. A little-c class
is not quite the same thing as a capital-C Class, nor is a little o-object quite the same thing as a capital-O Object. You learn
about the methods of the java.lang.Object class and how you use and override those in classes you write.
! You learn that Strings are immutable arrays of Unicode characters. You also learn that StringBuffers are growable arrays
of Unicode characters and that behind the scenes, many String operations are performed with StringBuffers.
! One-dimensional arrays of primitive data types are contiguous blocks of memory containing primitive values. However,
one-dimensional arrays of reference data types contain only references to objects that live elsewhere in the heap;
Multidimensional arrays are arrays of arrays, and all arrays implicitly subclass java.lang.Object.
! Finally, you learn about the internal structure of several common Java container classes: Vector, Stack, and Bitset. By
understanding how these classes operate, you can now make intelligent decisions about when and whether these classes are
appropriate for your needs and when you should rewrite or replace them with classes of your own devising.
Chapter 4
The Java Virtual Machine
J ava source code files are compiled into .class byte code files. The .class file will often be available for a class but the
corresponding .java source code file will not be. In these cases, with a little effort, it’s possible to derive an astounding amount of
information from the .class file alone.
Does the program in Listing 4-1 look familiar? I guarantee you’ve seen it before, probably many times.
CAFEBABE0003002D002008001D07001E07000E07001C0700160A0003000
9090004000A0A0005000B0C000C00150C0014001B0C001A001F01000770
72696E746C6E01000D436F6E7374616E7456616C75650100136A6176612F
696F2F5072696E7453747265616D01000A457863657074696F6E7301000F
4C696E654E756D6265725461626C6501000A536F7572636546696C65010
00E4C6F63616C5661726961626C6573010004436F64650100036F7574010
015284C6A6176612F6C616E672F537472696E673B29560100106A6176612
F6C616E672F4F626A6563740100046D61696E01000F48656C6C6F576F726
C642E6A617661010016285B4C6A6176612F6C616E672F537472696E673B2
9560100063C696E69743E0100154C6A6176612F696F2F5072696E7453747265616D3B010
0106A6176612F6C616E672F53797374656D01000C48656C6C6F
20576F726C642101000A48656C6C6F576F726C6401000328295600000002000500000000
000200090017001900010013000000250002000100000009B200071201B60006B1000000
0100100000000A00
0200000005000800030001001A001F000100130000001D00010001000000052AB70008B1
00000001
00100000000600010000000100010011000000020018
??æ___-_ __”__-____”___
___ ___
___@sr_____________println__
ConstantValue___java/io/PrintStream__
Exceptions___LineNumberTable__
SourceFile__LocalVariables___Code___out___(Ljava/lang/String;)V___java/lang/
Object___mai
n___HelloWorld.java___([Ljava/lang/String;)V___<init>___Ljava/io/PrintStream;___java/
lang/S
ystem__Hello World!__
HelloWorld___()V_____________ ___________%_______
=____?__±_________
______________________”________*?__±__________________________
That’s a little better. You can guess that this has something to do with Java because the word Java and various Java keywords seem
to show up. There’s also the string “Hello World” repeated a couple of times. This code isn’t very long, so just maybe this is a hello
world program. Then again, maybe not. Let’s look at this same program another way in Listing 4-3.
Method HelloWorld()
0 aload_0
1 invokespecial #6 <Method java.lang.Object.<init>()V>
4 return
Now we’re getting somewhere. This is obviously a class called HelloWorld. It extends java.lang.Object. The class has two methods.
The main() method is public static and void and takes an array of strings as arguments. The constructor HelloWorld() is public and
takes no arguments.
and
5 invokevirtual #8 <Method
java.io.PrintStream.print(Ljava/lang/String;)V>
Finally, let’s look at the same program one more way in Listing 4-4.
class HelloWorld {
System.out.println(“Hello World!”);
}
Listing 4-4 is obviously the classic Hello World program in Java, although it seems not nearly as complex as the last example.
Believe it or not, all four of these programs are the same, just viewed differently.
Listing 4-1 is pure hexadecimal and comes from the file HelloWorld.class. This is what you’d see by looking at the file with a disk
editor such as Norton Disk Editor. Listing 4-5 is a simple application that you can use to read files as hexadecimal digits.
import java.awt.*;
import java.io.*;
h.init();
h.resize(d.width/2, d.height/2);
h.move(d.width/4, d.height/4);
h.show();
public HexReader() {
super(“HexReader”);
if (e.target == OpenFile) {
File f = chooseFile();
printFile(f);
return true;
}
return false;
try {
output.setText(“”);
FileInputStream fin = new FileInputStream(f);
byte[] buffer = new byte[(int) f.length()];
int bytesread = fin.read(buffer);
output.setText(hexprint(buffer));
}
catch (Exception e) {
}
The main() method initializes the Frame shown in Figure 4-1. It has a TextArea field called output where the actual hex data appears
and a single button with the label “Open File.” When the users click the Open File button, they see a file dialog box in which they
can choose a file. The program passes the chosen file to the printFile() method. The printFile() method opens the file, connects an
input stream to it so that the contents can be read, and reads the contents into a byte array called buffer. Then the buffer is passed to
the hexprint() method to get a hexadecimal string that is displayed in the output TextArea.
Our main interest here is in the hexprint() method, so let’s take a closer look at it. The argument to hexprint() is a byte array, b. It
returns a string that contains a hexadecimal printout of those bytes. Each byte in the array is read in order and converted to two
hexadecimal digits.
To convert a byte to two hex digits, it is first split into its first four bits (b[i] >> 4) and its last four bits (b[i] & 0x0000000F). The
result of each of these calculations is an int. This int is passed to BitsToChar(). BitsToChar() is little more than a switch statement
that converts a single int between 0 and 15 to a hexadecimal digit between 0 and F. Numbers outside the acceptable range (greater
than 15 or less than 0) cause a new IllegalArgumentException to be thrown. This is a RuntimeException, so you don’t need to catch
or declare it. Each of the chars is appended to the temporary StringBuffer sb. Finally sb.toString() is returned.
A raw hex dump of a file is not very informative, although you can learn a little from it. All Java .class files should begin with the 4-
byte magic number 0xCAFEBABE — that is -889,275,714 in decimal. If you don’t see this number at the beginning of the file, then
you know it’s not a valid Java byte code file, even if the file name ends in .class.
Bytes four and five of a .class file (the two bytes immediately following CAFEBABE) show the minor version of the compiler that
produced this file. The two bytes after that show the major version of the compiler. In this example, the minor version is 0x0003 (3),
and the major version is 0x002D j(45). When a Java virtual machine reads a .class file, it checks to see if it understands that version
of the format. A virtual machine can generally read all of the minor versions in a major version, but if the major version changes, a
new virtual machine is required. Some virtual machines may also understand older major versions, but they should not attempt to
read files with newer major versions. The .class file format is actually more stable than the language and the API. Both Java 1.0.2
and Java 1.1 use the 45.3 .class file format.
The remaining digits all have meanings, but pulling them out by hand is excruciatingly painful. In fact, even in total disaster
situations (such as when your hard disk has crashed, taking with it three months of un-backed-up, mission-critical .class files while
the corresponding .java files are completely lost), you would probably copy the byte codes out of the file by hand and manually enter
them into another computer where you could decompile them.
The next variant, Listing 4-2, was obtained by forcing the file open in a text processor (specifically BBEdit). The printout here has
thrown away a few characters, such as page break (ASCII), that would have completely screwed up the formatting of this book. This
looks awful, but it’s a quick-and-dirty way to get a look at the String constants in a file.
I remember a stock trader in the early days of PCs who didn’t like one of the messages that his program gave him, so he opened up
the DOS .exe file in WordPerfect, searched for the offending string, replaced it, and saved the file. Amazingly, the program worked
with his “user modification.” I do not recommend this as general practice. There’s not much else to be learned here, so let’s move on.
Listing 4-3 is composed of disassembled byte code. This is much more useful than Listing 4-2. You get to see the name of the class,
all imported classes, and all methods and fields. With a little effort, you can learn to read the byte codes as you would read someone
else’s source code. This listing was produced with the JDK’s javap program with the -c command line flag — that is:
% javap -c HelloWorld
I’ll develop a different byte code disassembler later in this chapter and the next.
Why would you want to do this when you can look at the .java source code file instead? The short answer is that you’ll almost never
do this instead of looking at the .java file. However, it’s not uncommon to want to investigate the code of a class for which you do
not have original source code. You’ve probably become accustomed to using your Web browser’s View Source command to find out
how someone did a neat HTML trick. With the techniques and tools you’ll develop in this chapter, you’ll have an effective View
Source equivalent for Java .class files.
Let’s begin by looking at the source and byte codes for the main() method of the HelloWorld class.
System.out.println(“Hello World!”);
0 getstatic #7 <Field java.lang.System.out Ljava/io/PrintStream;>
3 ldc #1 <String “Hello World”>
5 invokevirtual #8 <Method java.io.PrintStream.print(Ljava/lang/String;)
V>
8 return
}
Lining them up side-by-side, you can see that these four lines
System.out.println(“Hello World!”);
See if you can figure out how. The numbers on the left of each line start counting at zero. They’re indices into the byte codes for this
method. This is just a series of bytes in a particular place in memory.
The first byte is an instruction: getstatic. The argument to this instruction is #7. This refers to the seventh entry in the constant pool
for this class. It so happens that the seventh entry in this particular pool is java.lang.System.out, an instance of java.io.PrintStream.
You know that System.out is a static field in the System class of type PrintStream, so it seems logical to interpret getstatic as a
command to retrieve a reference to a static class. In this case, it retrieves a reference to the System.out class and places it on the stack.
The next instruction is ldc, which stands for “load constant.” It has one argument: the integer constant #1. The 1 tells it which
constant to load from the constant pool. In this case, it loads the first constant in the pool, which happens to be the string “Hello
World!” Because “Hello World” is the only constant literal in HelloWorld.java, it’s not surprising that it’s the first one in the pool. A
reference to this string is placed on the top of the stack.
The next instruction is invokevirtual. This instruction calls instance methods. In this case, it calls the eighth entry in the constant
pool, the method println() of the java.io.PrintStream class. The arguments for println() are taken from the stack. In this case, the top
of the stack has a reference to the String object HelloWorld. The object whose method it should call is one level deeper in the stack.
That’s the reference to System.out placed there by getstatic.
The last instruction is return. There was no return statement in main(), but Java puts one here anyway. Writing return is optional in
void methods. The compiler is smart enough to add a blank return for you if your void method requires one. However, in a non-void
method, you have to return explicitly, because the compiler, although it knows you have to return, does not know what value you
want to return.
The next method is the constructor HelloWorld(). The .java source code file did not include a constructor. However, the compiler
puts a default constructor that takes no arguments in the byte code anyway.
Method HelloWorld()
0 aload_0
1 invokespecial #6 <Method java.lang.Object.<init>()V>
4 return
The aload instruction loads a reference from a local variable. In this case, it loads the zeroth local variable. This local variable is the
string “java.lang.Object()”. This becomes the argument to the next instruction. The next instruction, invokespecial, is used to call the
superclass’s constructor from the subclass’s constructor. Finally, the return instruction transfers control back to the calling method.
What’s most interesting about this method is that none of it is in the Java source code. All Java classes have a constructor that takes
no arguments if there are no other explicit constructors. Furthermore, all constructors call their superclass’s constructor before they
do anything else, even if there isn’t an explicit super() call in the first line of the subclass’s constructor.
In the remainder of this chapter, you’ll explore the Java .class file format to see how you change raw hexadecimal bytes such as
those in Listing 4-1 into something more intelligible like the byte code in Listing 4-4.
A Java .class file has 16 parts. Eleven of the parts always occupy the same number of bytes. For example, the magic number
0xCAFEBABE is always four bytes, never two bytes and never eight bytes. Five of the parts are of varying length. For example,
longer methods must have more byte codes than shorter methods. Table 4-1 lists the 16 parts of every Java .class file in order. These
parts always occur in exactly this order. The first step to disassembling a Java .class file is to break it up into these parts.
magic 4 This identifies the .class file format. It should be 0xCAFEBABE. If it’s
anything else, you’re dealing with a format more recent than this book.
minor version 2 The minor version of the compiler
major version 2 The major version of the compiler
constant pool variable The first two bytes give the number of entries in the constant pool.
Then, as many bytes as are necessary to fill that many entries are read.
The constant pool is a table of constant values used by this class.
access flags 2 These bit flags tell you whether the class is public, final, abstract, an
interface, and a few other things.
this class 2 This tells you which entry in the constant pool holds this class’s class
info.
superclass 2 If this is zero, then this class’s only superclass is java.lang.Object.
Otherwise, this is an index into the constant pool for the superclass
class info.
interfaces variable The interface table holds two byte indices into the constant pool table,
one for each interface that this class implements. The first two bytes
give the number of entries in the interface table. Therefore, after
reading the first two bytes, you have to read twice as many bytes as the
number stored in the first two bytes.
fields variable The fields table includes one field’s info structure for each field in the
class.
methods variable The method table contains the byte codes for each method in the class,
the return type of the method, and the types of each argument to the
method.
attributes 2 The attributes of the class
Listing 4-6 is a skeleton of a program that will disassemble byte code files. It reads the name of a file from the command line, opens
a FileInputStream to the file, chains a DataInputStream to that FileInputStream, and then proceeds to read bytes out of the file. Or at
least it will as soon as the skeleton is filled out. It would be simple enough to add a graphical interface to this program, as I did with
Listing 4-5, but let’s leave that as an exercise for you to explore.
Listing 4-6 Disassembler skeleton
import java.io.*;
import java.awt.FileDialog;
import java.awt.Frame;
DataInputStream theInput;
PrintStream theOutput;
try {
Disassembler d = new Disassembler();
d.disassemble();
}
catch (Exception e) {
System.err.println(e);
e.printStackTrace();
}
this(chooseFile(), os);
this(chooseFile(), System.out);
try {
readMagic();
readMinorVersion();
readMajorVersion();
readConstantPool();
readAccessFlags();
readClass();
readSuperclass();
readInterfaces();
readFields();
readMethods();
readAttributes();
// Output the file
writeImports();
writeAccess();
writeClassName();
writeSuperclass();
writeInterfaces();
writeFields();
writeMethods();
theOutput.println(“}”);
theOutput.println(“\n/*\n” + thePool + “\n*/”);
}
catch (ClassFormatError e) {
System.err.println(e);
return;
}
int magic;
}
void readFields() throws IOException {
}
Previous Table of Contents Next
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
The key method of this program is disassemble(). This is the method that actually reads the bytes. It does this by calling the 11
methods — readMagic(), readMinorVersion(), readMajorVersion(), readConstantPool(), readAccessFlags(), readClass(),
readSuperclass(), readInterfaces(), readFields(), readMethods(), and readAttributes() — in that order. You have to call them in that
order because, except for the first few parts, the parts don’t start on any particular byte. For example, to find where the seventh part
begins, you have to pick up where the sixth ended, and so on.
Once all the pieces have been read, you write them back out with writeImports(), writeAccess(), writeClassName(), writeSuperclass
(), writeInterfaces(), writeFields(), and writeMethods(). You must write these in the order in which they normally appear in a .java
source code file, not in the order in which they were read in the byte code file. Indeed, some of these parts, like the import
statements, are not specifically included in the compiled file but can be deduced from it.
In the event that there’s a problem with a .class file, a java.lang.ClassFormatError appears. Once this program encounters an invalid
file, it prints out an error message and stops executing.
It is rather unusual to catch an error rather than an Exception. You can’t normally recover from a ClassFormatError because it means
that one of the classes that the program needs will not be available. However, because you’re not loading the class, but just parsing it,
you have a little more leeway. This might get you into trouble if a ClassFormatError that you did not create yourself bubbles up
during the file parsing. This is rather unlikely, but a somewhat more robust solution would provide a means to distinguish between
the ClassFormatErrors that indicate a problem with the file being parsed and the ClassFormatErrors thrown by the Java VM when it
fails to load a requested class.
The following sections explain each part in detail and fill in the code needed to make these methods work. The next chapter, Chapter
5, expands the readMethods() method to provide a better analysis of the code inside method bodies.
Magic number
All Java .class files are supposed to begin with the four-byte magic number 0xCAFEBABE, that is -1,258,207,934 in decimal. If you
don’t see this at the beginning of the file, then you know that it’s not a valid Java byte code file, even if the file name ends in .class.
In this event, the disassembler should throw a ClassFormatError and bail out. The easiest way to read the number is with the java.io.
DataInputStream.readInt() method. Listing 4-7 shows the filled-out readMagic() method.
Listing 4-7 A method that reads the magic number and verifies that it is 0xCAFEBABE
int magic;
magic = theInput.readInt();
if (magic != 0xCAFEBABE) {
throw new ClassFormatError(“Incorrect Magic Number: “
+ magic);
}
Magic numbers are pure byte code phenomena. They do not appear anywhere in the .java source code file. Therefore, there’s no
corresponding writeMagic() method.
Minor version
Bytes four and five of a .class file (the two bytes immediately following CAFEBABE) are the minor version of the compiler that
produced this file. This is an unsigned 2-byte int (like a char) and can have a value between 0 and 65,535. This book documents
minor version 3. The easiest way to read an unsigned 2-byte int is with java.io.DataInputStream.read UnsignedShort() method.
Note: It’s important to realize that although readUnsignedShort() reads a short, it returns an int. A normal signed Java
short can’t hold values up to 65,535 as an unsigned short can.
minor_version = theInput.readUnsignedShort();
if (minor_version != 3) {
throw new ClassFormatError(“Minor Version not 3”);
}
Like magic numbers, minor and major versions are part of only the byte code, not the source code. Therefore, this method checks
only that the minor version is what it’s expected to be. It doesn’t need to be saved because it doesn’t have any effect on what comes
after it.
Major version
The next two bytes are the major version of the compiler. Like the minor version, the major version is a 2-byte, unsigned integer.
The major version you expect is 45. The parser is unlikely to be able to read anything different, so the program throws a
ClassFormatException and bails out. Listing 4-9 fleshes out this method:
major_version = theInput.readUnsignedShort();
if (major_version != 45) {
throw new ClassFormatError(“Major Version not 45”);
}
}
Constant pool
The constant pool is a data structure that stores all the constants in a program, not just literals like 1.0 or 72, but also class structures,
method references, and the names and types of variables. The disassembler needs to make frequent reference back to this data
structure when parsing later parts of the file. Many other entries in the .class file simply refer back to constants stored in the constant
pool. It is therefore necessary to create a data structure to hold the constants for later reference.
The largest difficulty in this endeavor is that the constant pool has to hold values of 11 different types. To make matters worse, six of
those types are reference types and five are primitive types. Because this is relatively complex, I’m going to push all the details into a
new class called ConstantPool. The Disassembler class will simply call the ConstantPool() constructor, as shown in Listing 4-10.
The ConstantPool() constructor will read the data out of theInput and parse it, and a new ConstantPool object will be stored in the
field thePool.
ConstantPool thePool;
The exact size of the constant pool depends on what’s inside it. The first two bytes of this part of the file are an unsigned short
specifying the number of entries in the constant pool. This number must be greater than zero. The first constant pool entry is reserved
for the virtual machine’s use. Therefore, there is actually one less than this number of actual entries to be read. However, entries of
different types can have different sizes.
Listing 4-11 is the ConstantPool class. This class does two things: first, it reads the constant pool from the file; second, it responds to
requests for items from the constant pool.
The ConstantPool class is implemented as an array of PoolEntry objects. Listing 4-12 is the PoolEntry class. A PoolEntry object can
hold one item that has one of the 11 different types and classes that can be stored in the constant pool. The ConstantPool constructor
first reads two bytes from the InputStream as an unsigned short. This specifies the number of entries in the ConstantPool so that it
can decide how large to make the PoolEntry array. Then, it passes InputStream to the PoolEntry() constructor enough times to fill the
array. The PoolEntry() constructor determines the type of that entry in the pool and reads the right number of bytes for that type.
To read a particular entry from the constant pool, you call the properly typed read method of ConstantPool — for example,
readDouble(int i) to get a double constant from the pool. These methods retrieve the right PoolEntry from the array and then call that
entry’s matching read method.
All information about the type of a PoolEntry is stored in the PoolEntry itself. The user, however, will generally need to know the
type of the entry being requested. If the user requests the wrong type from a PoolEntry, then the PoolEntry will throw a
ClassFormatError.
Note: This is not the only way that I could have structured this program. Another possibility would have been to make
PoolEntry an abstract class with an abstract read method. There would be subclasses for double, float, ClassInfo, and the
other types.
import java.io.*;
PoolEntry[] thePool;
return result;
The PoolEntry class begins with 11 constants to represent the 11 types that may appear in the constant pool. That is, every constant
pool entry is preceded by one unsigned byte that signals its type.
The PoolEntry() constructor reads this tag to determine how many bytes it should read. It reads four bytes for integer, float,
ClassInfo, FieldRef, MethodRef, NameAndType, and InterfaceMethodRef types. It reads eight bytes for long and double types.
ClassInfo and String types take two bytes. Finally, the UTF8 type requires a variable number of bytes, so first you must read one
more unsigned short to learn how many bytes are in the UTF8 structure. Once you know how many bytes you need to read, reading
them is almost trivial. Just use the read(byte[] b) method of the DataInputStream.
Note: I decided to store the data as a byte array that will be converted to the appropriate type when requested. I could
have performed the conversion immediately in the constructor and stored the converted values rather than the raw bytes.
However, this would require many excess fields for each PoolEntry object. For example, if a PoolEntry object is a float,
then the UTF8, integer, long, double, and all other fields would be empty. This seems excessively wasteful. However, if
you anticipate repeatedly requesting the same entry from the constant pool, then you might want to trade off the extra
memory in exchange for reduced CPU time.
import java.io.*;
int tag;
byte[] data;
tag = dis.readUnsignedByte();
int bytesToRead;
switch (tag) {
case cLong:
case cDouble:
bytesToRead = 8;
break;
case cInteger:
case cFloat:
case cFieldRef:
case cMethodRef:
case cNameAndType:
case cInterfaceMethodRef:
bytesToRead = 4;
break;
case cClassInfo:
case cString:
bytesToRead = 2;
break;
case cUTF8:
bytesToRead = dis.readUnsignedShort();
break;
default:
throw new ClassFormatError(“Unrecognized Constant Type “ + tag);
}
switch (tag) {
case cLong:
return “long “ + String.valueOf(readLong());
case cDouble:
return “double “ + String.valueOf(readDouble());
case cInteger:
return “int “ + String.valueOf(readInteger());
case cFloat:
return “float “ + String.valueOf(readFloat());
case cFieldRef:
return “FieldRef “ + String.valueOf(readFieldRef());
case cMethodRef:
return “MethodRef “ + String.valueOf(readMethodRef());
case cNameAndType:
return “NameAndType “ + String.valueOf(readNameAndType());
case cInterfaceMethodRef:
return “InterfaceMethodRef “ + String.valueOf(readInterfaceMethodRef());
case cClassInfo:
return “ClassInfo “ + String.valueOf(readClassInfo());
case cString:
return “String “ + String.valueOf(readString());
case cUTF8:
return “UTF8 “ + readUTF8();
default:
throw new ClassFormatError(“Unrecognized Constant Type”);
}
The PoolEntry class has 11 methods to return values. Each of these methods first checks to make sure that the type requested is in
fact the type of this object. If the type doesn’t match, then a ClassFormatError is thrown. Once it verifies the type, it converts the data
array into a primitive type or object of the appropriate type. In four cases, a new class is required to hold the return type.
The ClassInfo class holds a tag and an index into the constant pool for the name of the class. It appears in Listing 4-13.
int nameIndex;
int tag;
The RefInfo class, shown in Listing 4-14, holds indices for the class and the NameAndType in the constant pool. This is used for
method references, field references, and interface method references.
int classIndex;
int nameAndTypeIndex;
int tag;
Finally, the NameAndType class shown in Listing 4-15 holds indices into the constant pool for a name and a descriptor.
int nameIndex;
int descriptorIndex;
int tag;
Access flags
The access flags listed in Table 4-2 are stored in the .class file as a 2-byte bit mask. Bit 15 (the ones bit) is set if the class is public.
Bit 11 (the sixteens bit) is set if the class is final. Bit 10 is set if invokespecial needs to treat the class specially. (Don’t worry too
much about that. I explain what that means in the next chapter.) Bit 6 is set if the class is an interface. Bit 5 is set if the class is
abstract. The remaining bits are not yet used.
These flags are not independent of each other. If bit 6 is set (this is an interface), then bit 5 must also be set, because all interfaces are
abstract. Similarly, a class cannot have both bits 11 and 6 set, because a final class can’t be abstract.
The unused bits in the access flags are reserved for future use. For now, you should ignore them when parsing the file. Listing 4-16
provides the filled-out code to read the access flags. Listing 4-16 also introduces several new boolean fields to allow later methods to
know the values of these flags.
short access_flags;
boolean isPublic;
boolean isFinal;
boolean isInterface;
boolean isAbstract;
boolean isSpecial;
access_flags = theInput.readShort();
isPublic = (access_flags & 0x0001) == 0 ? false : true;
isFinal = (access_flags & 0x0010) == 0 ? false : true;
isInterface = (access_flags & 0x0020) == 0 ? false : true;
isAbstract = (access_flags & 0x0200) == 0 ? false : true;
isSpecial = (access_flags & 0x0400) == 0 ? false : true;
if (isAbstract && isFinal) {
throw new ClassFormatError(“This class is abstract and final!”);
}
if (isInterface && !isAbstract) {
throw new ClassFormatError(“This interface is not abstract!”);
}
if (isFinal && isInterface) {
throw new ClassFormatError(“This interface is final!”);
}
There are a few things to note about this code. First, it is necessary to make an explicit comparison with == and ?: to zero in order to
convert the masked short to a boolean. In a language like C or C++, you would simply take zero to mean false.
The next thing to ask yourself is whether the final if clause is really necessary. Given that this code will throw an error if a class is
abstract and final or if a class is an interface and not abstract, can it possibly reach the test for being both final and an interface?
thisClass
Next is a 2-byte unsigned short that is an index into the constant pool. At that index in the constant pool, you should find a ClassInfo
structure. This ClassInfo structure represents the current class or interface that you’re parsing. Listing 4-17 reads this index and
stores the ClassInfo structure it references in a new field: thisClass. Notice how we have to refer back to the constant pool at this
point.
ClassInfo thisClass;
Superclass
Immediately following the index of this class, you’ll find the index into the constant pool for the ClassInfo structure of this class’s
superclass (Listing 4-18). Reading this value is almost identical to the previous method. However, if this class does not have a
superclass (that is, if this is java.lang.Object, the only class without a superclass), then the index into the constant pool will be zero.
You therefore have to watch out for this special case. If the index is zero, then you should set superclass to null.
ClassInfo superclass;
Interfaces
A single class can implement multiple interfaces. First, an unsigned short tells you how many interfaces that this class implements
(possibly zero). There are exactly that many unsigned short indices in the constant pool. Each index points to a ClassInfo structure
for the implemented interface. Listing 4-19 is the fleshed-out readInterfaces() method. The interfaces are read, resolved, and stored in
a new field array called interfaces.
ClassInfo[] interfaces;
Attributes
The last thing you read from a .class file is the class’s attributes. Before you get to a class’s attributes, you have to read its fields and
methods. However, each field and method also has its own attributes table. Therefore, you should develop the classes needed to read
attributes before you need them. This class will read the attributes of the fields, the methods, and the class itself.
An attribute table consists of a specified number of attribute_info structures (see Listing 4-20). Each attribute_info structure consists
of one unsigned short that is the name index for this attribute. It’s an index into the constant pool. Next, there’s a 4-byte unsigned int
that gives you the length of the attribute’s data. Finally, there’s an array of data.
import java.io.*;
int nameIndex;
byte[] data;
Listing 4-21 is a filled-in readAttributes() method for the Disassembler class. An array of AttributeInfo structures holds the different
attributes.
AttributeInfo[] attributes;
Fields
After you’ve read the interfaces, you next read the class’s fields. Some classes have no fields. For example, the HelloWorld program
has only a method. An unsigned short tells you how many fields there are in the class. Then you read that many FieldInfo structures
from the file. A FieldInfo structure is composed of five items.
The first unsigned short is the access flags for the field. These tell you whether the field is public, private, protected, static, final,
volatile, and/or transient. Table 4-3 lists the bit masks for each of these modifiers. As usual, the bit mask values are chosen so that
the bitwise operators can easily pick out individual values. Note that not all of the possible combinations of flags are allowed. For
example, a field cannot be both public and private. Each flag is exactly equivalent to a Java keyword, which may modify a field.
public 0x0001
private 0x0002
protected 0x0004
static 0x0008
final 0x0010
volatile 0x0040
transient 0x0080
The 2-byte unsigned short immediately following the access flags is the name index—that is, an index into the constant pool that
provides the field name’s location.
Next comes the descriptor index, another 2-byte unsigned short index into the constant pool. This points to a UTF8 structure, which
represents a field descriptor.
Next comes the attributes table for this field. You read this by passing the DataInputStream into the AttributeTable constructor.
Listing 4-22 is the full FieldInfo class.
import java.io.*;
int accessflags;
int nameIndex;
int descriptorIndex;
AttributeInfo[] attributes;
Here’s the fleshed-out readFields() method for the Disassembler class. It’s quite simple, because all the work goes on inside the
FieldInfo class.
FieldInfo[] fields;
Methods
The methods table is similar to the fields table. First, there’s an unsigned short to tell you how many methods there are. Then there’s
an array of method_info structures. As with the FieldInfo structure, this program keeps all the intelligence inside the MethodInfo
constructor. Listing 4-23 is the fleshed-out readMethods() method for the Disassembler class.
MethodInfo[] methods;
The MethodInfo structure is almost identical to a FieldInfo structure. In fact, the only difference is in the permitted values for the
access flags and the meaning of the attributes. Listing 4-24 is the MethodInfo class.
Now that the entire .class file has been read into memory and parsed, it can be output as more-or-less-legible source code. You do
not need to output items in the order in which they appeared in the .class file. For example, the first thing outputted will be any
import statements in the file. Then you’ll produce the access specifiers for the class and then the class name itself, followed by any
interfaces that the class implements. Next come the fields, and then the methods. Along the way, you’ll add in necessary syntax —
such as semicolons and keywords — that is normally present in source code but is not included in byte code.
To do this, the Disassembler class needs for eight more methods to be filled out:
writeImports();
writeAccess();
writeClassName();
writeInterfaces();
writeFields();
writeMethods();
Each of these methods will parse the data structures read in the first part of this chapter to collect the needed information.
Import statements
There’s no one place in a .class file where all the import statements are stored. To determine which import statements were in the
source code, you have to list all the classes in the constant pool. You might choose to output one import statement for each class, or
you might be somewhat more selective. In this example, I have chosen not to produce import statements for the class itself or any
classes in java.lang. This makes the disassembled source code more similar to what you actually write in programs. If you wanted to,
you could include import statements only for entire packages (for example, import java.util.*) rather than for individual classes.
However, I find it convenient to be able to see exactly what classes a particular class references.
To find the classes, you loop through the constant pool and check each entry to see if it’s a ClassInfo structure. It’s important to
remember that the zeroth entry in the constant pool is not included in the .class file. When a ClassInfo structure is found, you use its
nameIndex() method to get the class’s name as a UTF8 structure from the constant pool. Each name thus retrieved is tested to be sure
that it’s not the name of this class and that it’s not a class from java.lang. Assuming neither of these is the case, an import statement
for the class is printed. Listing 4-25 demonstrates the writeImports() method.
PoolEntry pe = null;
String thisname = thePool.readUTF8(thisClass.nameIndex());
// recall that there’s nothing in the zeroth pool entry
for (int i = 1; i < thePool.howMany(); i++) {
pe = thePool.read(i);
if (pe.tag() == PoolEntry.cClassInfo) {
ClassInfo ci = pe.readClassInfo();
String name = thePool.readUTF8(ci.nameIndex());
name = name.replace(‘/’,’.’);
postedif (!name.startsWith(“java.lang.”) && !name.equals(thisname)) {
theOutput.println(“import “ + name + “;”);
}
}
}
theOutput.println();
Access specifiers
The writeAccess() method looks at the access specifiers for the class and prints them in Java form. Listing 4-26 has the code.
Note that if a .class file is not an interface, then it must represent a class. Note also that one access flag, isSpecial, has no equivalent
in Java source code. It exists only for the use of the compiler and the virtual machine.
The next thing you want to know is the name of the class. You can easily retrieve this from the thisClass field, which points to the
name of the class in UTF8 format in the constant pool (see Listing 4-27).
Next, you want to find out which class this class extends (see Listing 4-28). You have to watch out for the special case of java.lang.
Object, which has no superclass. Otherwise, this is very similar to the previous method.
if (superclass.nameIndex() != 0) {
String name = thePool.readUTF8(superclass.nameIndex());
theOutput.print(“extends “ + name + “ “);
}
Interfaces
The interfaces are similar except that there may be more than one of them. When you’re finished outputting all the interfaces, open
the class with an opening brace. The writeInterfaces() method is shown in Listing 4-29.
if (interfaces.length > 0) {
String name = thePool.readUTF8(interfaces[0].nameIndex());
theOutput.print(“implements “ + name + “ “);
for (int i=1; i < interfaces.length; i++) {
name = thePool.readUTF8(interfaces[i].nameIndex());
theOutput.print(“, “ + name);
}
}
theOutput.println(“ {“);
}
I’ve chosen to put the access specifiers, the class name, the class that this extends, all interfaces that this class implements, and the
opening brace on a single line of the file. This produces output that looks like:
Feel free to adjust this to match your preferences. For example, some people prefer to write each of these on separate lines.
Both versions produce identical byte code, so when you’re working backward from the byte code, there’s no way to distinguish the
two cases.
Fields
Only two parts of the file are left: the fields and the methods. Let’s look at the fields first. It’s not at all uncommon for a class to have
many fields. You therefore need to loop through all the fields with a for loop. Inside the loop, you check the access specifiers, the
name, and the type of each field.
To read the name of the field, you simply read the UTF8 structure in the constant pool at the field’s name index. The type of the field
requires more effort. Although it is stored as a UTF8 string in the constant pool at the FieldInfo’s descriptorIndex, the UTF8 string
needs to be decoded first. Primitive types like int or char are encoded as single letters. For example, an int is the capital letter I. Table
4-4 lists the encodings for the primitive types.
B byte
C char
D double
F float
I int
J long
S short
Z boolean
Class types are encoded as the capital letter L, followed by the fully qualified class name, followed by a semicolon. Furthermore, for
historical reasons, the periods in the fully qualified class name change to forward slashes. Therefore, inside the constant pool, the
String class is written as Ljava/lang/String;, the Object class is written as Ljava/lang/Object;, the Vector class is written Ljava/util/
Vector;, and so on. Converting this into the format you expect is easy. Just trim the first and last characters of the string with the
substring() method and use the replace() method to change the slashes to periods like this:
String s = “Ljava/lang/String;”;
String r = d.substring(1, r.length() - 1);
r = r.replace(‘/’, ‘.’);
The final type you need to deal with are the array types. These are encoded by prefixing the type of the array with left bracket signs
([), one for each dimension in the array. Thus, a double[] array is encoded as [double. A String[][] array is encoded as [[Ljava/lang/
String;. To decode array types, you first count the number of left brackets and then recursively call the decodeDescriptor() method.
Listing 4-30 shows the complete decodeDescriptor() method. It takes a single argument — the string to be decoded — and returns
the decoded string.
Now that you have a method to decode descriptors, it’s easy to finish the writeFields() method. Listing 4-31 demonstrates.
theOutput.println(“;”);
Note: I debated whether to include the code to read a field info structure and convert it into a string in the FieldInfo class
or in the Disassembler class. Although it would make somewhat more sense to encapsulate the code in the FieldInfo class,
it can be decoded only if each FieldInfo object carries a reference to its constant pool.
Previous Table of Contents Next
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
Methods
The final piece of the disassembly puzzle is decoding the methods. As with the fields, this will take place inside a loop, because
almost all classes have multiple methods. Every method has five parts that you must decode: the access specifiers, the return type,
the name, the argument list, the exception list, and the byte codes. Here’s a skeleton for the writeMethods() method:
theOutput.println();
theOutput.print(“ “);
// access specifiers
//argument list
//exceptions
// method body
The access specifiers are quite simple to read with the methods of the MethodInfo class. Here’s the code:
The arguments and return type are considerably harder to get at. The method descriptor contains a complete list of all of a method’s
arguments and its return value. These are encoded much like the field type descriptor, except that there can be more than one at a
time. The arguments appear in parentheses and the return value follows that. For example, a method with the signature
The getReturnType() method (see Listing 4-32) reads the descriptor and passes everything after the closing parenthesis to the
decodeDescriptor() method. This is the same decodeDescriptor() method used to get the type of a field. For example, if the method
descriptor is (DD)D, then the string “D” is passed to decodeDescriptor(). If the method descriptor is ([Ljava/lang/String)V, then the
string “V” is passed to decodeDescriptor().
The getArguments() method is more complex because it needs to parse several arguments at a time. Furthermore, there are no
convenient separators between the types. Finally, to make matters even worse, different types can have different sizes in the method
descriptor strings. Primitive and void types are always one character wide, but array and object types have undetermined sizes.
Therefore, you must consider the character to decide what to do with it. If the character is one for a primitive data type, then you
should pass that character (after converting it to a string) to the decodeDescriptor() method. However, if that character is an L, then
you need to read up to the next semicolon and pass that string to decodeDescriptor(). Finally, if a character is a left bracket, then you
must read as many brackets as follow and then read a type that may be a single character (that is, a primitive data type) or an object
type. In essence, you need to embed the method inside itself to properly handle array types.
Listing 4-33 is the getArguments() method. This uses the variable a to keep track of the number of arguments that have been
processed (so that you can tell where commas are needed in the argument list). It uses the variable i to tell which character in the
descriptor begins the next type. This method would be much simpler if the descriptor had a constant with format that allowed a and i
to be kept in sync.
return result;
The .class file also tells you which checked exceptions a method can throw. A checked exception is one that you must catch or
declare in a throws clause. The exceptions declared in the throws clause of a method are an attribute of the method. The
ExceptionsAttribute class, Listing 4-34, holds an array of indices into the constant pool, each of which points to a ClassInfo
structure. The ClassInfo structure represents the class of the exception that’s thrown.
import java.io.*;
int nameIndex;
int[] exceptions;
Secret: In the Disassembler class, the getExceptions() method returns a throws clause for a particular method. The
exceptions, if any, are stored in an attribute of the method with the name “Exceptions.” This attribute does not necessarily
exist. Methods that declare no exceptions will not have an Exceptions attribute.
Secret: This differs from what appears in the Java Virtual Machine Specification. According to that document, “There
must be exactly one Exceptions attribute in each method info structure.” However, current Java compilers do not write an
exceptions attribute in the method _ info structure unless the method actually has a throws clause. Listing 4-35
demonstrates the getExceptions() method.
ExceptionsAttribute theExceptions=null;
String result = “”;
return result;
The one piece left is the code inside the methods. This is the one piece of a Java .class file that you can’t easily make to match the
source code. That’s because the Java source language in which you write programs is compiled to the much lower level byte code.
In this chapter, I only show you where the bytes of the byte code are stored so that you can output them in a disassembly. The next
chapter, however, discusses what those byte codes mean, how you can read and understand them, and how you can work backward
from the byte codes to Java source code.
The byte codes for each method are stored in a Code attribute for the method. The Code attribute has many different fields, but most
of them are used only when interpreting code. In this chapter, you see only the actual byte codes.
The constructor has more information to parse than you need immediately. The toString() method converts the signed bytes in the
code array to integers between zero and 255. Listing 4-36 shows this CodeaAttribute class.
import java.io.*;
int nameIndex;
int maxStack;
int maxLocals;
byte[] code;
int startpc;
int endpc;
int handlerpc;
ExceptionTable[] exceptions;
AttributeInfo[] attributes;
}
This class makes reference to another class called ExceptionTable. Listing 4-37 shows this class. It provides information to the
virtual machine about where exception handlers begin and end. You won’t actually need it until the next chapter. However, this
information is included in the .class file, so you have to read it now.
int start_pc;
int end_pc;
int handler_pc;
int catch_type;
public ExceptionTable (int start_pc, int end_pc, int handler_pc, int catch_type) {
this.start_pc = start_pc;
this.end_pc = end_pc;
this.handler_pc = handler_pc;
this.catch_type = catch_type;
The getCode() method in the Disassembler class is particularly simple. It just needs to find the Code attribute of the method and call
its toString() method. Listing 4-38 demonstrates.
}
break;
}
}
if (theCode != null) {
return theCode.toString();
}
return “”;
Legal Issues
Many software companies want to tell you that it is illegal to disassemble, decompile, or reverse-engineer code. This is flatly wrong.
The courts in the United States have decided more than once that this is permissible. (Laws outside the United States may be
different. Consult a local attorney if this is a matter of concern.) Because the sort of reverse engineering described here is permitted
by law, many companies try to prevent it through copyright, patent, or licensing restrictions.
Copyright protects the expression of an idea, not the idea itself. Copyright does not prevent you from reusing an idea. Thus, if you
discover a neat algorithm by investigating the byte codes for SuperDuperApplet.class, just because SuperDuperApplet is copyrighted
does not mean you cannot reuse the algorithm in your own programs. Although it is illegal to copy the byte code verbatim and paste
it into your own files, it is perfectly legal to rewrite and recompile the algorithm.
A patent is a more serious level of protection. Software patents protect ideas, not merely the expression of ideas. If an organization or
individual owns a patent on an algorithm — RSA encryption, for example — then, you are legally required to license the patent from
the patent owner before using the algorithm in your own software.
Finally, many companies attempt to protect ideas through licensing. For example, the license for Developer Release 1 of Natural
Intelligence’s Roaster states that the licensee may not “reverse-engineer, decompile, disassemble, modify, translate, make any
attempt to discover the source code of the Software, or create derivative works from the Software.” This is fairly standard boilerplate
in software licensing agreements. Interestingly, a similar clause is not part of the license agreement from Sun for Java 1.0.2. To the
best of my knowledge, no one has tested this sort of clause in court, and I cannot offer an educated opinion as to whether it is
enforceable. Those aspects of shrink-wrap licenses that people have tested in court tend to relate to matters already covered under
copyright law (such as the making of additional copies), so even the validity of shrink-wrap licenses in general is in doubt.
To make matters even more confusing, the laws in 49 of the 50 states are often slightly different from each other. (The laws in
Louisiana are wildly different. I am not familiar with laws relating to this in Puerto Rico, Washington, D.C., or other non-state
territories in the United States, but I doubt they’re as different as Louisiana’s.) There is something called the Uniform Commercial
Code, UCC for short, which is an effort to get the laws of 49 states to conform to each other. (Louisiana law is really just too
different to be included.) As of late 1996, the UCC is being revised. The commercial software industry is trying to have provisions
written into the new UCC that would increase the validity and enforceability of shrink-wrap licenses. It remains to be seen what will
happen.
One problem with laws like the UCC is that they have a very hard time keeping up with the fast-changing software industry and the
Internet. Even if the new UCC does clarify the status of shrink-wrap licenses, it probably will not address the fact that most software
downloaded from the Internet does not have shrink-wrap! Some packages like Sun’s JDK display a splash screen with the license
agreement the first time that a user launches the software with buttons for the user to accept or reject the agreement. To my
knowledge, no one has tested such splash screen licenses in court. Even if they are held to be valid, what about splash screens that
provide only an “Agree” button and no “Disagree” button? What if programmer A agrees to the license, but later programmer B
starts using that computer and never sees the license? (For that matter, this applies to shrink-wrapped software, too.)
Furthermore, http servers allow .class files to be downloaded from many sites with no license of any sort. My suspicion is that no
more than copyright law protects these .class files, but I would not be surprised to see a software company dispute this in court.
As you can see, these issues are quite complex. There are few easy answers. However, on a practical note, nobody is going to know
or care if you disassemble a file to satisfy your personal curiosity or expand your knowledge. On rare occasions, companies have
gotten perturbed and called out their lawyers when a competitor released a product that could read their file formats. They also tend
to be annoyed when a writer publicly reveals information that they’d rather keep private. However, even in these cases there’s
relatively little they can do besides write threatening letters.
This discussion has been necessarily brief. Table 4-5 lists some more resources on the Internet for investigating these intellectual
property matters.
So far in this chapter I’ve assumed that you’re working with a single .class file. In practice, that’s not always true. Sometimes it takes
a little work to get a .class file to disassemble. First, you may have to do a little work to retrieve the .class file from a remote Web
server. Second, .class files are often distributed as parts of larger zip or jar archives. However, it’s not hard to extract the necessary .
class file from an archive.
Most Web browsers play Java applets when they encounter them. They do not save them onto your hard drive in an easily retrievable
form. Downloaded .class files may or may not be present in your browser’s cache. However, whether or not a browser caches a .class
file on disk, you can use the following trick to download a copy of the file you want.
Let’s suppose you’ve seen a cool applet on a Web site at http://www.idgbooks.com/example.html and you want to learn how the
programmer wrote it. Of course you’ll need a copy of the applet’s .class files. But how do you get them? This will take a little work,
but it’s really not hard. Here are the steps.
1. Use your Web browser’s View Source command to see the HTML for the Web page. You’re looking for the
<APPLET> tag like this:
2. Write a very simple Web page that includes an HREF link to the file you want to look at it. For example,
3. Load the page with the HREF link to the file into your Web browser. Then, use the Save this Link As command in the
pop-up menu to save the file on your hard drive, as shown in Figure 4-2. That’s it. You should now have a clean copy of
the .class file to work with.
Figure 4-2 The Save this Link as menu command in the Macintosh version of Netscape.
Zip files
Most VMs include their class libraries in the form of uncompressed zip archives called classes.zip. If you want to poke around in the
innards of the class library, the first thing you need to do is unzip this file.
Theoretically, it shouldn’t matter whether a package hierarchy is or is not zipped, as long as your CLASSPATH is set up properly. In
practice, that’s not always true. Before dearchiving someone else’s file for experimentation, you should always copy it to a directory
that’s not in your CLASSPATH. It’s best not to work on the original copies.
As soon as the Sysops had installed JDK 1.1 on sunsite.unc.edu, I copied the classes.zip file to a test directory of my home directory
and then unzipped it to start poking around. In other words,
% mkdir ~/test
% cp /usr/local/java-1.1/lib/classes.zip ~/test
% unzip ~/test/classes.zip
There are many tools available for unzipping .zip files. Unzip is the dearchiver of choice for zip files on UNIX. StuffIt Expander
works well on the Macintosh. Although PKZip is the original zip program, it cannot handle the long filenames that Java requires.
Therefore, on Windows platforms, you should use WinZip instead.
Jar files
Java 1.1 introduced Jar files. Jar is a rough concatenation of “Java archive.” Jar files can contain all the .class files, image files,
sound files, and other files needed to run an applet. By placing all these different files into a single file, a Web browser can download
them with only a single request to the Web server. Depending on the server’s load and network conditions, this can save from a few
milliseconds to several minutes of time. Furthermore, a Jar file can compress its contents so the savings can be even larger.
Jar files are included on Web pages with applet tags that look like this:
You can download a Jar file to your hard drive exactly the same way you’d download a .class file. Once you have the Jar file on your
local hard drive, you need to dearchive it to retrieve the individual parts. The java.util.zip package includes classes that can parse and
handle Jar files. Sun’s JDK 1.1 for Windows and Solaris includes a command line jar program based on this package that you can
use to pack and unpack Jar files. An equivalent program will likely be available for the Macintosh by the time this book hits store
shelves.
The jar command line syntax (see Table 4-6) is deliberately similar to the classic UNIX tar command. Options are passed as one-
character flags that follow the word jar on the command line. Archiving versus dearchiving is chosen through the c (create) or x
(extract) flag, not via jar and unjar commands as a PKZip user might expect.
Option Purpose
For example, to archive all files in the current directory you would type
To archive just some files and directories, specify them by name on the command line like this:
Directories are archived recursively; in other words, their immediate contents and the contents of any sub-directories are archived.
Summary
In this chapter, you learn about the format of Java .class files and how to read them. In particular, you learn the following:
! You can view the same program in different formats: pure hexadecimal bytes, ASCII text, disassembled byte code, and
Java source code. The first three formats are available in the .class file. The last is the form of the .java source code file.
! How a .class file is organized and how to split it into its component parts.
! How to work backward from the compiled .class byte code file to an approximation of the source that generated it. This
task will continue in the next chapter.
! That there are legal issues involved in doing this. Although copyright doesn’t prevent reverse engineering any more than
it prevents you from reading a book you’ve bought, it may prevent you from copying what you’ve learned verbatim.
Patents may provide more serious restrictions.
! How to retrieve a .class file, wherever it resides, whether on a Web site, in a zip archive, or in a jar file.
In the next chapter, you learn how to decode and understand method bodies, instead of just printing them as streams of bytes.
Chapter 5
Java Byte Code
T he last chapter walked you through the disassembly of a Java .class file. However, I left one crucial part of the file — the bodies
of the different methods — in raw bytes rather than converting it back to .java source code. This large topic easily deserves a chapter
of its own.
Method bodies are one of the few areas in which it is not always possible to return to the source code format. For one thing, you
completely lose all of the information about the identifiers inside a method or in a method’s argument list in the compilation phase.
There’s no way to get it back. Furthermore, .java source code keywords like for and while don’t always map in a one-to-one fashion
onto the byte code equivalents. For example, for every for loop, there’s an equivalent while loop that produces the same byte code.
Given only the byte code, there’s no way to tell which one was in the source code. Finally, optimizing compilers essentially rewrite .
java source code before turning it into byte code. When you reverse the process, you get back the rewritten source code, not the
original.
Although fully accurate decompilation is, in general, not possible, with practice you can learn to understand what the byte codes say.
The fundamental problem is that there’s often more than one way to say the same thing. However, if you care only about the
meaning of what was said, rather than the exact way in which it was expressed, you can extract the meaning from the byte codes.
You learn to do this in this chapter.
Note: If debugging information is included in the file, then the .class file has a complete copy of the source code, and you
can get everything back. However, that is a relatively unchallenging and uninteresting case, so this chapter covers the
harder case in which no debugging information is included in the file.
The code in a method body is a linear sequence of bytes. Each byte has an unsigned value between zero and 255. The interpretation
of each byte depends on its position. The zeroth byte is always a .java byte code instruction (an opcode, for short). Bytes after that
point can be either byte code instructions or arguments for byte code instructions. For example, if the byte at position zero in a
method is 16, then the byte at position one is not a byte code, but rather a signed byte to be pushed onto the stack. Data values
embedded directly in the code like this are sometimes called literals.
The only way to distinguish between opcodes and literals is by starting with the instruction at position zero in the code array and
working forward. For example, if the byte zero is 16 and opcode 16 is known to take a one-byte argument, then byte one must be the
literal argument for the instruction and byte two must be the next opcode.
Most instructions have a precisely defined number of bytes that should be read as literals following the opcode itself. Opcode 16
always takes exactly one byte as an argument, never zero bytes and never more than one byte. Arguments that by their nature do not
take up a fixed number of bytes, such as arrays and strings, are not directly included in the code array. Instead, a 2-byte index into
the constant pool is given. Therefore, once you know the possible byte codes and their arguments, you can distinguish between
instructions and data. Java enforces the dichotomy between code and data very strictly. This is an essential feature of Java’s security.
Human beings aren’t very good at remembering the meanings of many small numbers like the 200-plus opcodes. Sun has therefore
defined mnemonic strings for these codes. These strings are listed in Table 5-1.
Although these mnemonics may seem just as cryptic as the raw bytes at first glance, they do have a logic to them. For example,
opcodes that begin with the letter i commonly operate on int types. Instructions that begin with the letter l operate on longs.
Instructions that begin with f operate on floats, and instructions that begin with d operate on doubles. Instructions that begin with a
operate on references. Opcodes that contain the word add generally add things. Opcodes that contain the word sub generally subtract
things. You get the idea. The rest of this chapter looks at what these instructions do. By the time you finish this chapter, these
mnemonics will make perfect sense to you.
In Chapter 4, you saw method bodies printed as raw bytes. Although it’s possible to use Table 5-1 and a good memory to understand
what those bytes mean, I certainly wouldn’t recommend it. Instead, I expand the CodeAttribute class so that it uses mnemonics
instead of raw bytes. This is shown in Listing 5-1.
Although Listing 5-1 looks frighteningly long, it is mostly one big switch statement that reads the next opcode and its arguments and
returns a string. Because there are more than 200 opcodes, the readCode() method has to be quite long. However, most of the cases
in the switch statement are very simple. Over half of them merely return an opcode. Most of the rest return an opcode and some
literal data.
The three exceptions are the cases for the lookupswitch, tableswitch, and wide instructions. These are special because they have
variable-length data encoded in the code array. I discuss them in more detail later in this chapter, when I discuss the meaning of
those instructions.
import java.io.*;
int nameIndex;
int maxStack;
int maxLocals;
byte[] code;
ExceptionTable[] exceptions;
AttributeInfo[] attributes;
public CodeAttribute(AttributeInfo ai) throws IOException {
nameIndex = ai.nameIndex();
ByteArrayInputStream bis = new ByteArrayInputStream(ai.data);
DataInputStream dis = new DataInputStream(bis);
maxStack = dis.readUnsignedShort();
maxLocals = dis.readUnsignedShort();
code = new byte[dis.readInt()];
dis.read(code);
exceptions = new ExceptionTable[dis.readUnsignedShort()];
for (int i = 0; i < exceptions.length; i++) {
exceptions[i] = new ExceptionTable(dis.readUnsignedShort(),
dis.readUnsignedShort(), dis.readUnsignedShort(),
dis.readUnsignedShort());
}
attributes = new AttributeInfo[dis.readUnsignedShort()];
for (int i = 0; i < exceptions.length; i++) {
attributes[i] = new AttributeInfo(dis);
}
}
}
return result.toString();
switch (opcode) {
case 0: return position + “ nop”;
case 1: return position + “ aconst_null”;
case 2: return position + “ iconst_m1”;
case 3: return position + “ iconst_0”;
case 4: return position + “ iconst_1”;
case 5: return position + “ iconst_2”;
case 6: return position + “ iconst_3”;
case 7: return position + “ iconst_4”;
case 8: return position + “ iconst_5”;
case 9: return position + “ lconst_0”;
case 10: return position + “ lconst_1”;
case 11: return position + “ fconst_0”;
case 12: return position + “ fconst_1”;
case 13: return position + “ fconst_2”;
case 14: return position + “ dconst_0”;
case 15: return position + “ dconst_1”;
case 16: return position + “ bipush “ + dis.readByte();
case 17: return position + “ sipush “ + dis.readShort();
case 18: return position + “ ldc “ + dis.readUnsignedByte();
case 19: return position + “ ldc_w “ + dis.readUnsignedShort();
case 20: return position + “ ldc2_w “ + dis.readUnsignedShort();
case 21: return position + “ iload “ + dis.readUnsignedByte();
case 22: return position + “ lload “ + dis.readUnsignedByte();
case 23: return position + “ fload “ + dis.readUnsignedByte();
case 24: return position + “ dload “ + dis.readUnsignedByte();
case 25: return position + “ aload “ + dis.readUnsignedByte();
case 26: return position + “ iload_0”;
case 27: return position + “ iload_1”;
case 28: return position + “ iload_2”;
case 29: return position + “ iload_3”;
case 30: return position + “ lload_0”;
case 31: return position + “ lload_1”;
case 32: return position + “ lload_2”;
case 33: return position + “ lload_3”;
case 34: return position + “ fload_0”;
case 35: return position + “ fload_1”;
case 36: return position + “ fload_2”;
case 37: return position + “ fload_3”;
case 38: return position + “ dload_0”;
case 39: return position + “ dload_1”;
case 40: return position + “ dload_2”;
case 41: return position + “ dload_3”;
case 42: return position + “ aload_0”;
case 43: return position + “ aload_1”;
case 44: return position + “ aload_2”;
case 45: return position + “ aload_3”;
case 46: return position + “ iaload”;
case 47: return position + “ laload”;
case 48: return position + “ faload”;
case 49: return position + “ daload”;
case 50: return position + “ aaload”;
case 51: return position + “ baload”;
case 52: return position + “ caload”;
case 53: return position + “ saload”;
case 54: return position + “ istore “ + dis.readUnsignedByte();
case 55: return position + “ lstore “ + dis.readUnsignedByte();
case 56: return position + “ fstore “ + dis.readUnsignedByte();
case 57: return position + “ dstore “ + dis.readUnsignedByte();
case 58: return position + “ astore “ + dis.readUnsignedByte();
case 59: return position + “ istore_0”;
case 60: return position + “ istore_1”;
case 61: return position + “ istore_2”;
case 62: return position + “ istore_3”;
case 63: return position + “ lstore_0”;
case 64: return position + “ lstore_1”;
case 65: return position + “ lstore_2”;
case 66: return position + “ lstore_3”;
case 67: return position + “ fstore_0”;
case 68: return position + “ fstore_1”;
case 69: return position + “ fstore_2”;
case 70: return position + “ fstore_3”;
case 71: return position + “ dstore_0”;
case 72: return position + “ dstore_1”;
case 73: return position + “ dstore_2”;
case 74: return position + “ dstore_3”;
case 75: return position + “ astore_0”;
case 76: return position + “ astore_1”;
case 77: return position + “ astore_2”;
case 78: return position + “ astore_3”;
case 79: return position + “ iastore”;
case 80: return position + “ lastore”;
case 81: return position + “ fastore”;
case 82: return position + “ dastore”;
case 83: return position + “ aastore”;
case 84: return position + “ bastore”;
case 85: return position + “ castore”;
case 86: return position + “ sastore”;
case 87: return position + “ pop”;
case 88: return position + “ pop2”;
case 89: return position + “ dup”;
case 90: return position + “ dup_x1”;
case 91: return position + “ dup_x2”;
case 92: return position + “ dup2”;
case 93: return position + “ dup2_x1”;
case 94: return position + “ dup2_x2”;
case 95: return position + “ swap”;
case 96: return position + “ iadd”;
case 97: return position + “ ladd”;
case 98: return position + “ fadd”;
case 99: return position + “ dadd”;
case 100: return position + “ isub”;
case 101: return position + “ lsub”;
case 102: return position + “ fsub”;
case 103: return position + “ dsub”;
case 104: return position + “ imul”;
case 105: return position + “ lmul”;
case 106: return position + “ fmul”;
case 107: return position + “ dmul”;
case 108: return position + “ idiv”;
case 109: return position + “ ldiv”;
case 110: return position + “ fdiv”;
case 111: return position + “ ddiv”;
case 112: return position + “ irem”;
case 113: return position + “ lrem”;
case 114: return position + “ frem”;
case 115: return position + “ drem”;
case 116: return position + “ ineg”;
case 117: return position + “ lneg”;
case 118: return position + “ fneg”;
case 119: return position + “ dneg”;
case 120: return position + “ ishl”;
case 121: return position + “ lshl”;
case 122: return position + “ ishr”;
case 123: return position + “ lshr”;
case 124: return position + “ iushr”;
case 125: return position + “ lushr”;
case 126: return position + “ iand”;
case 127: return position + “ land”;
case 128: return position + “ ior”;
case 129: return position + “ lor”;
case 130: return position + “ ixor”;
case 131: return position + “ lxor”;
case 132: return position + “ iinc “
+ dis.readUnsignedByte() + “ “ + dis.readUnsignedByte() ;
case 133: return position + “ i2l”;
case 134: return position + “ i2f”;
case 135: return position + “ i2d”;
case 136: return position + “ l2i”;
case 137: return position + “ l2f”;
case 138: return position + “ l2d”;
case 139: return position + “ f2i”;
case 140: return position + “ f2l”;
case 141: return position + “ f2d”;
case 142: return position + “ d2i”;
case 143: return position + “ d2l”;
case 144: return position + “ d2f”;
case 145: return position + “ i2b”;
case 146: return position + “ i2c”;
case 147: return position + “ i2s”;
case 148: return position + “ lcmp”;
case 149: return position + “ fcmpl”;
case 150: return position + “ fcmpg”;
case 151: return position + “ dcmpl”;
case 152: return position + “ dcmpg”;
case 153: return position + “ ifeq “ + dis.readShort();
case 154: return position + “ ifne “ + dis.readShort();
case 155: return position + “ iflt “ + dis.readShort();
case 156: return position + “ ifge “ + dis.readShort();
case 157: return position + “ ifgt “ + dis.readShort();
case 158: return position + “ ifle “ + dis.readShort();
case 159: return position + “ if_icmpeq “ + dis.readShort();
case 160: return position + “ if_icmpne “ + dis.readShort();
case 161: return position + “ if_icmplt “ + dis.readShort();
case 162: return position + “ if_icmpge “ + dis.readShort();
case 163: return position + “ if_icmpgt “ + dis.readShort();
case 164: return position + “ if_icmple “ + dis.readShort();
case 165: return position + “ if_acmpeq “ + dis.readShort();
case 166: return position + “ if_acmpne “ + dis.readShort();
case 167: return position + “ goto “ + dis.readShort();
case 168: return position + “ jsr “ + dis.readShort();
case 169: return position + “ ret “ + dis.readUnsignedByte();
case 170: // tableswitch
pad = 3 - (position % 4);
dis.skip(pad);
defaultByte = dis.readInt();
int low = dis.readInt();
int high = dis.readInt();
result = position + “ tableswitch “
+ defaultByte + “ “ + low + “ “ + high;
for (int i = low; i < high; i++) {
int newPosition = position + pad + 12 + (i-low)*4;
result += “\n” + newPosition + “ “ + dis.readInt();
}
return result;
case 171: // lookupswitch
pad = 3 - (position % 4);
dis.skip(pad);
defaultByte = dis.readInt();
int npairs = dis.readInt();
result = position + “ lookupswitch “ + defaultByte + “ “ + npairs;
for (int i = 0; i < npairs; i++) {
int newPosition = position + pad + 12 + i*8;
result += “\n” + newPosition + “ “
+ dis.readInt() + “ “ + dis.readInt();
}
return result;
case 172: return position + “ ireturn”;
case 173: return position + “ lreturn”;
case 174: return position + “ freturn”;
case 175: return position + “ dreturn”;
case 176: return position + “ areturn”;
case 177: return position + “ return”;
case 178: return position + “ getstatic “ + dis.readUnsignedShort();
case 179: return position + “ putstatic “ + dis.readUnsignedShort();
case 180: return position + “ getfield “ + dis.readUnsignedShort();
case 181: return position + “ putfield “ + dis.readUnsignedShort();
case 182: return position + “ invokevirtual “ + dis.readUnsignedShort();
case 183: return position + “ invokespecial “ + dis.readUnsignedShort();
case 184: return position + “ invokestatic “ + dis.readUnsignedShort();
case 185: return “invokeinterface “ + dis.readUnsignedShort() + “ “
+ dis.readUnsignedByte();
// 186 is unimplemented in Java 1.0.2
case 187: return position + “ new “ + dis.readUnsignedShort();
case 188: return position + “ newarray “ + dis.readByte();
case 189: return position + “ anewarray “ + dis.readUnsignedShort();
case 190: return position + “ arraylength”;
case 191: return position + “ athrow”;
case 192: return position + “ checkcast “ + dis.readUnsignedShort();
case 193: return position + “ instanceof “ + dis.readUnsignedShort();
case 194: return position + “ monitorenter”;
case 195: return position + “ monitorexit”;
case 196:
int nextCode = dis.readUnsignedByte();
switch(nextCode) {
case 132: // iinc
return “wide\n” + (position+1) + “ iinc” + dis.readUnsignedShort()
+ “ “ + dis.readUnsignedShort();
case 21:
return “wide\n” + (position+1) + “ iload” + dis.readUnsignedShort();
case 22:
return “wide\n” + (position+1) + “ lload” + dis.readUnsignedShort();
case 23:
return “wide\n” + (position+1) + “ fload” + dis.readUnsignedShort();
case 24:
return “wide\n” + (position+1) + “ dload” + dis.readUnsignedShort();
case 25:
return “wide\n” + (position+1) + “ aload” + dis.readUnsignedShort();
case 54:
return “wide\n” + (position+1) + “ istore”
+ dis.readUnsignedShort();
case 55:
return “wide\n” + (position+1) + “ lstore”
+ dis.readUnsignedShort();
case 56:
return “wide\n” + (position+1) + “ fstore”
+ dis.readUnsignedShort();
case 57:
return “wide\n” + (position+1) + “ dstore”
+ dis.readUnsignedShort();
case 58:
return “wide\n” + (position+1) + “ astore”
+ dis.readUnsignedShort();
} // end switch
case 197:
return “multianewarray “ + dis.readUnsignedShort() + “ “
+ dis.readUnsignedByte();
case 198: return position + “ ifnull “ + dis.readShort();
case 199: return position + “ ifnonnull “ + dis.readShort();
case 200: return position + “ goto_w “ + dis.readInt();
default: return position + “ unknown_opcode “ + opcode;
} // end switch
The nameIndex() and CodeAttribute() methods are the same ones that you saw in the preceding chapter. The readCode() method is a
big lookup table that returns the next opcode mnemonic and its arguments from the code array. Some instructions take one or more
of their arguments from the bytes that follow them in the code array. Therefore, you can’t just blindly convert each byte to a
mnemonic. Some bytes must remain bytes. Opcodes 170, 171, and 196 (tableswitch, lookupswitch, and wide) can take a varying
number of arguments, so extra logic is required to handle them. I describe this logic later in this chapter, when I discuss those
instructions.
Armed with this information, you should now have a better idea of what’s going on inside the methods when you disassemble a
program. For example, disassembling HelloWorld, you get the output shown in Listing 5-2.
import java.io.PrintStream;
0 getstatic 7
3 ldc 1
5 invokevirtual 8
8 return
void <init>() {
0 aload_0
1 invokespecial 6
4 return
/*
1: String 18
2: ClassInfo 19
3: ClassInfo 25
4: ClassInfo 26
5: ClassInfo 27
6: MethodRef ClassIndex: 4; NameAndTypeIndex: 9
7: FieldRef ClassIndex: 5; NameAndTypeIndex: 10
8: MethodRef ClassIndex: 3; NameAndTypeIndex: 11
9: NameAndType NameIndex: 14; DescriptorIndex: 12
10: NameAndType NameIndex: 29; DescriptorIndex: 22
11: NameAndType NameIndex: 30; DescriptorIndex: 13
12: UTF8 ()V
13: UTF8 (Ljava/lang/String;)V
14: UTF8 <init>
15: UTF8 Code
16: UTF8 ConstantValue
17: UTF8 Exceptions
18: UTF8 Hello World
19: UTF8 HelloWorld
20: UTF8 HelloWorld.java
21: UTF8 LineNumberTable
22: UTF8 Ljava/io/PrintStream;
23: UTF8 LocalVariables
24: UTF8 SourceFile
25: UTF8 java/io/PrintStream
26: UTF8 java/lang/Object
27: UTF8 java/lang/System
28: UTF8 main
29: UTF8 out
30: UTF8 print
*/
Listing 5-2 is the most intelligible disassembly yet. The rest of this chapter describes the various opcodes, so you can really
understand what’s going on inside the file.
Instructions require data on which to operate. You can take this data from five places: the method stack, the heap, the constant pool,
the local variable array, and the byte code itself. The method stack, the local variable array, and the byte code are specific to the
method. The constant pool and the heap are shared by all other threads and methods executing in the same virtual machine.
All of the operations in a method such as addition or subtraction take place on the stack. For example, to add two integers, the
integers are first pushed onto the stack, then they’re popped off of the stack, and then their sum is pushed back onto the stack.
The local variable array is a temporary holding area for local variables declared in the method and arguments passed to the method.
You have to copy those local variables onto the stack before you can do anything with them.
Instructions that access the heap get references to items in the heap from the stack. You learned about the constant pool in the last
chapter. Operations that access the constant pool also use indices into the pool placed on the heap.
When the Java virtual machine is running, each thread has a stack of frames. (This is related neither to HTML frames nor to the java.
util.Stack class.) A frame holds the local variables and arguments for a method and a working area for the method called the operand
stack. When a method is called, the virtual machine creates a new frame for the method with space for its local variables and its local
stack. This frame is placed on top of the thread’s stack. When the method completes, this frame is popped from the thread’s stack,
and the method’s return value (if any) is pushed onto the top of the calling method’s stack.
Each method operates on the values contained in its local variable array and on its method stack. You can think of the local variable
array as an array of 32-bit words, and the method stack as a stack of 32-bit words.
Let’s look at a simple example. Consider the following method, which adds two to four and returns the sum:
int a = 4;
int b = 2;
int c = a + b;
return c;
}
Here’s the same method as disassembled byte code:
0 iconst_4
1 istore_1
2 iconst_2
3 istore_2
4 iload_1
5 iload_2
6 iadd
7 istore_3
8 iload_3
9 ireturn
Let’s investigate this byte code instruction by instruction, to see what effect each one has on the array of local variables and the
stack.
You first need to look at the number of different locations referenced by load and store instructions to determine how many local
variables are used.
Instruction 0 pushes the constant 4 onto the stack. That doesn’t affect the local variable array at all.
Instruction 1 stores a value into position 1 in the local variable array. The local variable array therefore must have at least one entry.
Instruction 2 pushes the constant value 2 onto the method stack. Again, this has no effect on the local variable array.
Instruction 3 stores a variable into position 2 in the local variable array. Therefore, there are at least positions 1 and 2 in the local
variable array, so that array has to be at least 2 entries long.
Instructions 4 and 5 move values from the local variable array at positions 1 and 2 onto the stack. However, you’ve already seen
variables 1 and 2, so that’s nothing new. Instruction 6 adds the two variables onto the top of the stack and puts the result back on the
stack. Again, the local variable array is not changed.
Instructions 7 and 8 store something into the third position in the local variable array and then load it onto the stack. So there now
have to be at least 3 entries in the local variable array.
So far, we’ve counted three local variables in positions 1 through 3. However, in a non-static method, the zeroth position always
holds a reference to the current object. Therefore, there are four total local variables in positions 0 through 3.
Note: You can also get this information from the maxLocals field of the CodeAttribute class.
In this example, the source code contains three local variables — a, b, and c—and the byte code contains four local variables. The
number of local variables in the .java source code is not necessarily the same as the number of local variables used by the byte code.
The byte code may have more local variables if it needs temporary storage, or it may have fewer if some variables can be reused. The
other thing to note is that the names of the local variables are completely lost in compilation; only their types are known.
When the method is first called, an array of four words is allocated to hold the local variables. The zeroth element of the array is a
reference to this object. The first element of the array corresponds to the int variable a; the second element of the array
correspondents to the int variable b; and the third element of the array correspondents to the int variable c. The stack is initially
empty. Figure 5-1 demonstrates.
Figure 5-1 The local variable array and the stack when the method is loaded.
The zeroth instruction is iconst _ 4. The const family of instructions push values onto the operand stack. This instruction begins with
the letter i, so it pushes an int value. It ends with _ 4, so the value pushed is four. Thus, after this instruction has executed, the
operand stack is one word high, and the int 4 is on the top of the stack. Figure 5-2 shows the state of the local variable array and the
operand stack after this instruction is executed.
Figure 5-2 The local variable array and the stack after instruction 0.
The first instruction is istore _ 1. The store family of instructions pop values from the operand stack and store them in the local
variable array. This instruction begins with the letter i, so it stores an int value. It ends with _ 1, so the value is stored in local
variable 1. Therefore, 4 is popped off the stack and stored in local variable 1. After this instruction has executed, the operand stack is
empty. Figure 5-3 shows the state of the local variable array and the operand stack after this instruction is executed.
Figure 5-3 The local variable array and the stack after instruction 1.
The second instruction is iconst _ 2. This pushes the int 2 onto the stack. Figure 5-4 shows the state of the local variable array and the
operand stack after this instruction is executed.
Figure 5-4 The local variable array and the stack after instruction 2.
The third instruction is istore _ 2. It pops the top of the stack and stores it in local variable 2. After this instruction has executed, the
operand stack is empty. Figure 5-5 shows the state of the local variable array and the operand stack.
Figure 5-5 The local variable array and the stack after instruction 3.
The fourth instruction is iload _ 4. The load family of instructions pushes values from the local variable array onto the operand stack.
This instruction begins with the letter i and ends with _ 4, so it pushes the int 4.
Unlike popping a value from the stack, loading a value from the local variable array does not remove it from the array. Local
variable 1 still has the value 4 after this instruction is executed. Figure 5-6 shows the state of the local variable array and the operand
stack.
Figure 5-6 The local variable array and the stack after instruction 4.
The fifth instruction is iload _ 2. This pushes the second local variable onto the stack. After this instruction has executed, the operand
stack is two words high, and the int 2 is on the top of the stack. Figure 5-7 shows the state of the local variable array and the operand
stack.
Figure 5-7 The local variable array and the stack after instruction 5.
The sixth instruction is iadd. The add family of instructions pops two values from the operand stack, adds them, and pushes the result
back onto the stack. This instruction begins with the letter i, so it adds ints. After this instruction has executed, the operand stack is
one word high, and the int 6 is on the top of the stack. Figure 5-8 shows the state of the local variable array and the operand stack.
Figure 5-8 The local variable array and the stack after instruction 6.
The seventh instruction is istore_ 3. This pops the top of the stack and stores it in local variable 3. After this instruction has executed,
the operand stack is empty. Figure 5-9 shows the state of the local variable array and the operand stack.
Figure 5-9 The local variable array and the stack after instruction 7.
The eighth instruction is iload_ 3. This pushes the value 6 from local variable 3 onto the top of the stack. After this instruction has
executed, the operand stack is one word high, and the int 6 is on the top of the stack. Figure 5-10 shows the state of the local variable
array and the operand stack.
Figure 5-10 The local variable array and the stack after instruction 8.
The ninth and final instruction in this method is ireturn. This instruction pops the int from the top of the stack and returns it to the
method that calls this method. There’s no picture here because this instruction destroys the frame, so there are no more local
variables or stack words after this instruction is executed.
You may have noted that the seventh and eighth instructions weren’t strictly necessary. The value 6 was on the top of the stack after
instruction 6 and could have been returned then. This would be equivalent to rewriting the .java source code without the intermediate
variable c, like this:
int a = 4;
int b = 2;
return a+b;
An optimizing compiler might have noticed this and omitted the seventh and eighth instructions. A very good optimizing compiler
could have noticed that this method uses only constants and always returns 6. It would have thus rewritten the source code like this:
return 6;
In fact, this is exactly what javac -O does. The following is the byte code emitted by javac with the -O flag to indicate that it should
perform optimization.
0 bipush
1 6
2 ireturn
The bipush instruction sign-extends the next byte in the code array to an int and pushes it onto the stack. The ireturn instruction in
byte 2 then returns that int from the top of the stack. By using the optimizer, you’ve reduced nine instructions to three, a saving of 66
percent in both time and space. This is one reason why the names of the local variables are not stored in the byte code. By the time
an optimizer is through with the code, there may not be any variables left.
The Opcodes
There are more than 200 different opcodes in the Java virtual machine. You certainly don’t need to memorize all of them. I suggest
that you skim over this section to get a feel for how the different classes of opcodes behave. Then return here for reference when you
need more details about a particular opcode that you’ve encountered in a disassembly.
Nop
Nop is short for “no operation.” When the virtual machine encounters a nop instruction, it does nothing and moves to the next
instruction. Neither the stack nor the local variable array is affected.
I’ve never actually seen a nop instruction appear in .java byte code. It is probably a holdover from other architectures in which nop
instructions were used to ensure code alignment.
The instructions in this section push values onto the stack. This usually precedes some other instruction that uses these values as
arguments.
The 15 const instructions push frequently occurring constants onto the operand stack. The mnemonics for these instructions all take
the form
where type is one of a, i, l, or f and value is the value pushed onto the stack. Thus, iconst _ 2 pushes the int 2 onto the stack, and
fconst _ 1 pushes the float 1.0 onto the stack. iconst_m1 pushes -1 onto the stack, and aconst_null pushes the null reference onto the
stack.
The bipush instruction pushes a signed byte constant onto the stack. It operates on the byte in the code array immediately following
itself. It sign-extends the byte to an int and pushes it onto the stack.
The sipush instruction pushes a signed short constant onto the stack. It takes the short from the two bytes of the code array
immediately following itself. As with everything else in Java, these bytes are in Big-Endian order. The instruction sign-extends the
short to an int and pushes it onto the stack.
The ldc codes
The abbreviation ldc stands for “load constant.” The three ldc codes copy values from the constant pool onto the stack.
The ldc instruction interprets the byte that follows it in the code array as an unsigned index into the constant pool. If that entry in the
constant pool is a float or an int, then that value is copied onto the stack. However, if that entry in the constant pool is a string (that
is, if it is an index to a UTF8 structure), then a new String object is constructed and initialized to the value of the UTF8 structure.
Then a reference to this new String object is placed on the stack.
The ldc_w instruction is the same, except that it uses a 2-byte unsigned index into the constant pool. The ldc instruction is used when
the desired constant is somewhere between index 0 and index 255. Larger indices require the ldc_w instruction. You can think of
ldc_w as an abbreviation for “load constant wide.”
The ldc2_w instruction copies an 8-byte long or double value from the constant pool into the top two words of the stack. The two
bytes of the code array immediately following the ldc2_w instruction are interpreted as an unsigned short index into the constant
pool.
Stack manipulation
Several instructions operate directly on the words on the stack, without concerning themselves with what those words mean.
The pop instruction removes or “pops” the top word from the stack and does nothing with it. The word is completely lost. This
instruction can be used only when a word length quantity like an int or a reference is on the stack. You can’t use it when there’s a
two-word type like a double or a long on the stack. For those types, you must use the pop2 instruction, which pops two words from
the stack and discards them. You can also use the pop2 instruction to remove two words that contain ints or floats or references from
the stack. However, the Java virtual machine does not allow this (or any other) instruction to split the two words of a long or a
double.
The three dup instructions duplicate a word from the stack and put the copy back in the stack. They differ as to where in the stack
they put the copy. The three dup2 instructions duplicate the two words on the top of the stack and put those words back in the stack.
They, too, differ as to where exactly they put the words back in the stack. These are the only instructions in the virtual machine that
put words somewhere other than on the top of the stack. All of these instructions enforce the integrity of longs and doubles. That is,
they do not allow you to move half of a two-word quantity or to move a word between the two words in a long or a double.
The dup instruction copies the top word on the stack and puts the copy on the top of the stack. Figure 5-11 shows one possible stack
before and after the dup instruction.
Figure 5-11 The stack, before and after the dup instruction.
The dup_x1 instruction copies the top word on the stack and puts the copy two words down in the stack. This forces the two words
that were on the top of the stack to each move up one place. If the stack is as shown on the left side of Figure 5-12, then after the
dup_x1 instruction has been executed, the stack will be in the state shown on the right of Figure 5-12.
Figure 5-12 The stack, before and after the dup_x1 instruction.
The dup_x2 instruction copies the top word on the stack and puts the copy three words down in the stack. This forces the three words
on the top of the stack to each move up one. If the stack is as shown on the left side of Figure 5-13, then after the dup_x2 instruction
has been executed, the stack will be in the state shown on the right of Figure 5-13.
Figure 5-13 The stack, before and after the dup_x2 instruction.
The dup2 instruction copies the top two words on the stack and puts the copies on the top of the stack. Figure 5-14 illustrates this.
Figure 5-14 The stack, before and after the dup2 instruction.
The dup2_x1 instruction copies the top words on the stack and puts the copies three words down in the stack. Figure 5-15
demonstrates.
Figure 5-15 The stack, before and after the dup2_x1 instruction.
Finally, the dup2_x2 instruction copies the top words on the stack and puts the copies four words down in the stack. Figure 5-16
demonstrates.
Figure 5-16 The stack, before and after the dup2_x2 instruction.
swap
The swap instruction swaps the two words on the top of the stack. That is, the word on the top of the stack moves down one and the
word immediately below the top of the stack moves up one, to the top. The size of the stack does not change. Figure 5-17 illustrates
this. As always, the swap instruction cannot be used to split or reverse the two words in a long or a double.
By far, the largest number of byte codes are the load and store instructions. There are more than 60 of these. The load instructions
load a variable onto the stack from the local variable array. The store instructions pop a value from the stack and stores it into the
local variable array.
Although there are more than 60 of these instructions, they’re quite easy to understand. All of the load instructions act very much
alike. They differ primarily in the type of value that each one loads and secondarily in how they determine the local variable to load.
Similarly, all the store instructions act the same. They also differ in the type of value that each one stores and in how they determine
the local variable into which to store values.
Each of these instructions begins with a letter that indicates the type of value on which it operates. For example, instructions that
begin with the letter l operate on longs, and instructions that begin with f operate on floats. Table 5-2 lists these mappings between
first letters and types.
Letter Type
a reference
b byte or boolean
c char
d double
f float
i int
l long
s short
Furthermore, instructions that begin with one of these letters followed by the letter a operate on arrays of the type. Thus, iload loads
an int value from the local variable array onto the stack, but iaload loads a value from an array of ints onto the stack. (I discuss arrays
in more detail later.)
Instructions that end with an underscore (_) followed by a small integer (0, 1, 2, or 3) operate on the local variable at that point in the
local variable array. Otherwise, the next byte in the code array is used as an unsigned index into the local variable array. Thus,
istore_2 pops a value from the stack and stores it in local variable 2. However, istore first reads another byte from the code array to
determine which local variable it should use to store the value that it pops from the top of the stack.
There are 12 basic instructions that work with the local variable array: aload, iload, fload, lload, dload, istore, fstore, astore, lstore,
dstore, ret, and iinc. Each of these instructions is followed by a single, unsigned byte. This byte determines which local variable is
used. The use of a single byte is very space-efficient. However, it does place an upper limit of 256 local variables, fewer if some of
them are longs or doubles. Sometimes this isn’t quite enough.
The wide instruction allows access to many more local variables — up to 65,536. When wide precedes any of the first 11 of these
instructions (that is, any except ret), the instruction uses the next two bytes in the code array, rather than only one byte, as an index
into the local variable array.
For example, istore 2 normally means to pop an int from the stack and store it in local variable 2. However, if the instruction before
istore is wide, then you have to read an extra byte. Thus, wide istore 2 8 means to pop an int from the stack and store it in local
variable 520. Figure 5-18 demonstrates.
The wide instruction has an even larger effect on the iinc instruction. Recall that normally the iinc instruction is followed in the code
array by two bytes, an index into the local variable array and the constant increment. When preceded by wide, both of these are
widened to 2-byte shorts, the first unsigned and the second signed. Thus, wide iinc 0 2 2 0 means “increment local variable 2 by
512.”
Arithmetic
Arithmetic byte codes come in two groups: the binary operators and the unary operators. Most of the common operators that you’re
familiar with, such as + and *, are binary operators. This means that they take two arguments. For example, you always add numbers
two at a time. There’s no way to add just a single number, even when you write something like this:
int a = 3 + 7 + c;
Java first adds the 3 and the 7. Then it adds c to their sum. That is, it splits the calculation into two parts, like this:
int temp = 3 + 7;
int a = temp + c;
In fact, you do this sort of splitting implicitly when you add a series of numbers yourself by hand.
The only unary arithmetic operator in Java is the minus sign (-). This operator changes the sign of a variable. Thus, if the int variable
a is 7, then -a is -7. This can be a little confusing, because the - character serves two other purposes in Java. It is also the binary
subtraction operator in expressions like 3 - 7 and can be a part of numeric literals like -7 or -98.6. Although these appear to be the
same thing in .java source code, they are three different things in .java byte code.
All byte code binary arithmetic instructions operate on two values of the same type. In other words, ints can be added only to other
ints, floats to other floats, and so on. You can’t add an int and a double or multiply a float times a long. If you need to combine two
types in one expression, you must first use one of the type conversion operators that we discuss later.
There are four addition instructions: iadd, ladd, fadd, and dadd. These add ints, longs, floats, and doubles respectively.
The instructions iadd and fadd pop two words off the stack, add them, and push the sum back onto the stack. After the instruction has
executed, the stack is one word shorter. Figure 5-19 shows the addition of two ints — 4 and 2 — to get 6.
The instructions ladd and dadd pop four words from the stack, add them, and push a two-word result back onto the stack. After the
instruction has executed, the stack is two words shorter. Figure 5-20 shows the addition of the long values 4L and 2L to get 6L.
Subtraction is similar to addition. There are four subtraction operators — isub, lsub, fsub, and dsub — one each for ints, longs, floats,
and doubles. Unlike addition, subtraction is not commutative; that is, a - b is not, in general, the same as b - a. Therefore, it’s
important to note that the value on the top of the stack is subtracted from the value immediately below it, and not the other way
around. Figure 5-21 and Figure 5-22 demonstrate this.
Figure 5-21 4 - 2 = 2.
Floats are subtracted exactly the same way. Longs and doubles are too, except that each number requires two words. Figure 5-23
shows what happens on the stack when you subtract 2L from 4L.
Figure 5-23 4L - 2L.
There are four multiplication instructions: imul, lmul, fmul, and dmul. These multiply ints, longs, floats, and doubles, respectively.
The instructions imul and fmul pop two words off the stack, multiply them, and push the product back onto the stack. After the
instruction has executed, the stack is one word shorter. Figure 5-24 shows the multiplication of two ints — 4 and 2 — to get 8.
The instructions lmul and dmul pop two two-word values from the stack (a total of four words), multiply them, and push a two-word
result back onto the stack. After the instruction has executed, the stack is two words shorter. Figure 5-25 shows the multiplication of
the long values 4L and 2L to get 8L.
By now, this process should seem familiar. You can probably guess that there are four division instructions, that they are idiv, ldiv,
fdiv, and ddiv, and that they divide ints, longs, floats, and doubles, respectively.
Division, like subtraction, is not commutative. Ten divided by five is two, but five divided by ten is one half. (In Java, 5 / 10 is
actually 0, because integer division truncates toward zero.) The first value popped from the stack is divided into the second number
popped from the stack. In other words, the first number popped from the stack is the dividend, and the second number popped from
the stack is the divisor. Figures 5-26 and 5-27 demonstrate this.
Figure 5-26 5 / 10 = 0.
Figure 5-27 10 / 5 = 2.
Floats are divided exactly the same way, except that no truncation towards zero is necessary. Longs and doubles are also, except that
each number requires two words. Figure 5-28 shows what happens when you divide 10L by 5L.
The rem codes are used for the Java remainder operator, %. As usual, there are four of them — irem, lrem, frem, and drem — one
each for ints, longs, floats, and doubles. Like division, taking the remainder is not commutative. 10 % 2 is 0, but 2 % 10 is 2. The
first value popped from the stack is divided into the second number popped from the stack, and the remainder is pushed onto the
stack. Figures 5-29 and 5-30 demonstrate.
Figure 5-30 10 % 2 = 5.
Floats are handled exactly the same way. Longs and doubles are too, except that each number requires two words. Figure 5-31 shows
what happens when you take the remainder of 10L by 2L.
The final group of arithmetic instructions is different. These are the neg instructions — ineg, lneg, fneg, and dneg — for ints, longs,
floats, and doubles, respectively. Each of these pops only one value from the stack, not two. The instructions ineg and fneg pop one
32-bit word. The instructions lneg and dneg pop two 32-bit words. In either case, the value is negated; that is, its sign is changed, and
the result is pushed back onto the stack. This is semantically equivalent to multiplying by -1, although in general, using the negation
operator will be faster. The size of the stack does not change. Figure 5-32 shows the negation of the int value 255. Figure 5-33 shows
the negation of the long value -32.
Bit manipulation
The bit-level byte code instructions map very closely to the bit-level operators. For each bit-level operator like << or ~, there are
exactly two instructions, one for ints and one for longs. (Remember that you can’t use bit-level operators on floats, doubles, or
references.)
Shift operators
There are six shift operators: ishl, lshl, ishr, lshr, iushr, and lushr. Each pops two operands from the stack and returns the result to the
stack. The instructions beginning with i operate on ints, and the instructions beginning with l operate on longs.
Shift left
The ishl and lshl instructions correspond to the << operator. They’re also used by the <<= operator. The first value popped from the
stack is the number of bits to shift left. The second value popped from the stack is the value that will be shifted. The vacated bits are
filled with zeroes. When an ishl instruction has completed, the stack is one word shorter. When an lshl instruction has completed, the
stack is two words shorter. Figure 5-34 shows the int value 255 being left-shifted eight places. Figure 5-35 shows the long value -
32L being left-shifted eight places.
The ishr and lshr instructions correspond to the >> and >>= operators. The first value popped from the stack is the number of bits to
shift right. The second value popped from the stack is the value that will be shifted. The vacated bits are filled with the sign bit — 0
for positive numbers and 0 or 1 for negative numbers. When an ishr instruction has completed, the stack is one word shorter. When
an lshr instruction has completed, the stack is two words shorter. Figure 5-36 shows the int value 255 being right-shifted four places.
Figure 5-37 shows the long value -32L being right-shifted eight places.
The iushr and lushr instructions correspond to the >> and >>= operators. The first value popped from the stack is the number of bits
to shift right. The second value popped from the stack is the value that will be shifted. The vacated bits are filled with zeroes,
regardless of the sign of the number. When an iushr instruction has completed, the stack is one word shorter. When a lushr
instruction has completed, the stack is two words shorter. Figure 5-38 shows the int value -32 being unsigned-right-shifted eight
places.
Combination
The bitwise operators &, |, and ^ each have two corresponding byte code instructions, one each for ints and longs. These are iand,
land, ior, lor, ixor, and lxor. Each behaves exactly as you would expect. The int instructions pop two words off the operand stack,
combine them with the appropriate operator, and push the result back onto the stack. The long instructions pop four words off the
operand stack, combine them with the appropriate operator, and push two words back onto the stack. Figure 5-39 shows the
conjunction of the ints -32 and 8. Figure 5-40 shows the disjunction of the ints -32 and 8. Figure 5-41 shows the xor of the longs -32
and 255.
The iinc instruction isn’t absolutely necessary. There aren’t any Java source files that cannot be compiled without it. However, it
does allow loops to run much more quickly. The byte immediately after the iinc instruction in the code array is an unsigned index
into the local variable array. The byte after that is a signed byte by which the local variable will be incremented. The iinc instruction
is most commonly used to compile the ++, --, += and -= operators, especially in loops. The iinc instruction is the only one that
operates directly on a value in the local variable array without moving the value onto the stack first.
0 iconst_0
1 istore_1
2 goto
3 0
4 6
5 iinc
6 1
7 2
8 iload_1
9 bipush
10 20
11 if_icmplt
12 -1
13 -6
14 return
The zeroth instruction, iconst_0, pushes the int 0 onto the operand stack. The second instruction, istore_1, moves the int onto the top
of the stack into local variable 1. Therefore, local variable 1 is now 0. The second instruction, goto, reads the next two bytes as a
signed short telling how many bytes to jump over. Here, the value of the short is 6, so the goto jumps to instruction 8, iload_1. This
instruction pushes the int in local variable 1 onto the stack. Next, instruction 9, bipush, pushes the value 20 onto the stack. Instruction
11 pops the top two values from the stack and checks to see whether the second value popped (the int that was one down in the stack)
is less than the second value popped (the int that was on top of the stack). If it is, then the next two bytes are read as the signed short
address of the instruction to which control should jump. In this case, control jumps back six bytes, that is, to instruction 11 - 6, which
is instruction 5, iinc. The first byte after iinc is 1, and the second byte is 2, so 2 is added to local variable 1. Because local variable 1
was 0, it’s now 2. Now we’re back at instruction 7. Again, local variable 1 is pushed onto the stack, and the int constant 20 is pushed
onto the stack. Once again they’re compared, and control jumps back to instruction 5. Once again, local variable 1 is incremented by
2. It’s now 4. This continues until local variable 1 reaches the value 20. At that point, the comparison fails, and control moves to
statement 14, return.
The alternative way to compile this code, without the iinc instruction, takes many more instructions. You have to push extra values
onto the stack, add them there, and then store the result back in local variable 1, like this:
0 iconst_0
1 istore_1
2 goto 6
5 iload_1
6 iconst_2
7 iadd
8 istore_1
9 iload_1
10 bipush 20
12 if_icmplt
13 -1
14 -7
15 return
With the iinc instruction, the for loop is one byte and three instructions shorter than it would be otherwise.
Conversion codes
You may have noticed that all the instructions discussed so far operate only on values of the same type. For example, there are
instructions to add two ints, to add two floats, to add two longs, and to add two doubles. However, there is no instruction that adds an
int to a double, a float to a double, a float to a long, or any other combination of primitive data types. At the level of the virtual
machine, only values of the same type may be operated on or compared. Before a statement like “float f = 7.5 + 6;” can be compiled,
the int value 6 must be promoted to a float.
There are 14 conversion instructions. They all look like i2l or f2d. The first letter is the type from which you convert. The last letter
is the type to which you convert. Thus, i2l converts an int to a long, and f2d converts a float to a double. All conversions take place
as specified in the Java Language Specification and in Chapter 2 of this book.
Each of these instructions pops the appropriate number of words from the stack (one for an int or a float and two for a long or a
double) and pushes the converted value back onto the stack. The instructions are as follows:
For example, the method here is compiled to the byte codes that follow:
int i = 6;
double d = i;
return d;
Note in particular instruction 6, where the int value 6 on top of the stack is promoted to the double value 6.0 before being stored in
the local variable array. The other conversion instructions are used in exactly the same way.
0 bipush 6
2 istore_1
3 iload_1
4 i2d
5 dstore_2
6 dload_2
7 dreturn
Comparison instructions
There are 17 comparison instructions. Java has the most direct support for comparisons on ints. There are five instructions that
compare the int on the top of the stack to zero and branch accordingly. There are five more instructions that compare the top two ints
on the stack and branch accordingly. There are five comparison instructions that compare the top two longs, floats, or doubles on the
stack and push a 1 or 0 onto the stack, depending on the result. Finally, there are two instructions that compare references for
equality and branch depending on the result.
In all cases where the result of a comparison is a branch, the two bytes immediately following the comparison instruction are a
signed short giving the relative position in the code array to which it should branch. Positive values mean a jump forward. Negative
values mean a jump backward. For example, consider this instruction:
12 if_icmplt
13 -1
14 -7
Here the top two ints on the stack are popped and compared. If the second word popped is less than the first word popped, then
control branches back seven bytes before byte 12. If the second int popped is not less than the first int popped, then control moves
forward to byte 15, the next instruction after if_icmplt.
The first two, ifeq and ifne, are used primarily to add a branch after a comparison between two longs, floats, or doubles. For
example, consider the following method and its byte code equivalent:
double d1 = 5.6;
double d2 = 7.8;
if (d1 > d2) {
return 3;
}
else {
return 5;
}
0 ldc2_w
1 0
2 6
3 dstore_1
4 ldc2_w
5 0
6 9
7 dstore_3
8 dload_1
9 dload_3
10 dcmpl
11 ifle
12 0
13 5
14 iconst_3
15 ireturn
16 iconst_5
17 ireturn
The two doubles, 5.6 and 7.8, are compared with the dcmpl instruction in line 10. The result of that comparison, a 0 or a 1, is pushed
onto the stack. Then instruction 11, ifle, pops the result off the stack. If it’s less than or equal to zero, execution branches ahead five
bytes to byte 16, and 5 is returned. Otherwise, control continues with instruction 14, and 3 is returned.
Suppose that the top of the stack holds two int values, as shown in Figure 5-42. There are six instructions that pop these two ints and
branch depending on how they compare.
Comparisons between non-int data types aren’t as common, so there are fewer instructions to handle all the different cases.
Therefore, comparisons between non-int data types often take longer and result in larger code sizes.
There is exactly one instruction to compare two longs: lcmpl. It pops two longs from the stack. If the two longs are equal, lcmpl
pushes zero back onto the stack. If they’re not equal and the first long is greater than the second long popped, then -1 is pushed onto
the stack. Finally, if the second long popped is greater than the first long popped, 1 is pushed onto the stack. Depending on the actual
comparison that was made in the source code, one of the ifeq, ifne, ifgt, ifge, iflt, or ifle instructions would normally be used to test
this value and decide whether to branch.
The fcmpl, fcmpg, dcmpl, and dcmpg instructions behave almost exactly the same as lcmpl except that they operate on floats and
doubles. Each of these four instructions pops the top two values from the stack. If they’re not equal and the first value popped is
greater than the second value popped, then -1 is pushed onto the stack. Finally, if the second value popped is greater than the first
value popped, 1 is pushed onto the stack. However, they differ in what they do with NaN values. Recall that NaN is unordered. Any
comparison with NaN must return false. If either value popped is NaN, then fcmpg and dcmpg push 1 onto the stack, and fcmpl and
dcmpl push -1 onto the stack. These two variations are needed to handle NaN comparisons properly.
Reference comparisons
There are four comparison instructions that are used with reference data types. Greater than and less than comparisons make no sense
for references, but you can compare references for equality. The if_acmpeq instruction branches if the two references on the top of
the stack are equal. The if_acmpne instruction branches if they’re not equal. The ifnull instruction branches if the reference popped
from the top of the stack is null. The ifnonnull instruction branches if the reference popped from the top of the stack is not null. All
four of these instructions read the next two bytes after the instruction as a signed short giving the relative offset to which they should
branch.
Unconditional branching
There are five instructions that branch unconditionally. The instructions goto and goto_w are commonly used to compile while, for,
and do-while loops. The instructions jsr, jsr_w, and ret are used to compile finally clauses. The goto and jsr instructions read the next
two bytes in the code array as a signed short specifying the location to which it should branch. The goto_w and jsr_w use the next
four bytes as a signed int specifying the locations to which they should branch. The jnr, jsr_w, and ret instructions are covered in the
section on Exceptions later in this chapter.
Switching
Although a switch statement is logically equivalent to a sequence of if-else if-else if-…-else statements, Java provides special
support to allow it to be executed more efficiently. The tableswitch instruction is used when the cases mostly cover a range of
integers. For example:
switch (j) {
case 0:
case 1:
case 2:
case 3:
case 5:
default:
}
The lookupswitch instruction is used when the cases are further apart. For example:
switch (j) {
case 0:
case -121:
case 236:
case 342:
case 5:
default:
}
Both the tableswitch and lookupswitch instructions are variable-length instructions. They are the only such instructions in the Java
virtual machine. Each of these instructions is followed in the code array by some padding and a table of locations of instructions to
which they should branch. This table is called a jump table. The two instructions differ in how the jump table is stored.
Each of these instructions requires that its table be aligned on a 4-byte boundary in the code array. In other words, the jump table
always starts on byte 4, 8, 12, 16, 20, or some other multiple of four. Between zero and three null bytes are added as padding
immediately following the switch instruction, to ensure that the jump table is 4-byte aligned.
tableswitch
Following the tableswitch instruction and the zero-to-three padding bytes are three 4-byte, signed int values. The first is called
default; the second is called low; and the third is called high. After these three ints, there’s a jump table of relative offsets. Each entry
in the jump table is a signed, 4-byte int that is an address of an instruction to jump to relative to the tableswitch instruction. For
example, if a jump table entry is 72 and the tableswitch is at instruction 17, then when that jump table entry is chosen, control jumps
to instruction 72+17, which is instruction 89. There are always exactly high - low + 1 entries in the jump table. Because each jump
table entry takes four bytes, the jump table is 4 * (high - low +1) bytes long.
The index of the jump table entry to jump to is popped from the stack. If this number is between low and high inclusive, then the
appropriate jump table entry is read, and control moves to the instruction indicated by that jump table entry. Otherwise, the default
value is added to the address of the tableswitch instruction, and control jumps to that location. For example, consider the tswitch
method:
public int tswitch(int i) {
int result = 0;
switch (i) {
case 0:
result += 2;
case 1:
result += 3;
case 2:
result += 4;
case 3:
result += 6;
case 5:
result += 7;
default:
}
return result;
Without optimization, tswitch() is compiled to the byte codes shown in Listing 5-3. The tableswitch instruction is byte 3. The next
byte is byte 4, so no padding bytes are needed. Bytes 4 through 7 are the default value, in this case 52. Bytes 8 through 11 are the low
value in the jump table, here 0. Bytes 12 through 15 are the high value in the jump table, here 5. Remember that the lowest value in
the switch statement was 0 and the highest was 5. Therefore, there will be six entries in this jump table, entries 0 through 5.
The jump table itself is stored in bytes 16 through 39. This is not a large method or a large switch statement, so it’s easy to read the
different ints by just looking at the last byte in each 4-byte set. The jump table entries are 37, 40, 43, 46, 52, and 49 — in that order.
Thus, if the int value 0 is popped from the stack, control jumps to 37+3, that is instruction 40, iinc. If the int value 1 is popped from
the stack, control jumps to instruction 40+3 or 43, a different iinc instruction.
You may have noticed that the instructions for each jump are not disjointed. Once a jump takes place, all subsequent instructions are
executed. That’s because I left break statements out of the cases to simplify the code. If 0 is passed into this version of tswitch(), 2+3
+4+6+7=22 is returned. I add break statements in the next section.
0 iconst_0
1 istore_2
2 iload_1
3 tableswitch
4 0
5 0
6 0
7 52
8 0
9 0
10 0
11 0
12 0
13 0
14 0
15 5
16 0
17 0
18 0
19 37
20 0
21 0
22 0
23 40
24 0
25 0
26 0
27 43
28 0
29 0
30 0
31 46
32 0
33 0
34 0
35 52
36 0
37 0
38 0
39 49
40 iinc
41 2
42 2
43 iinc
44 2
45 3
46 iinc
47 2
48 4
49 iinc
50 2
51 6
52 iinc
53 2
54 7
55 iload_2
56 ireturn
Control should never jump out of the current method, nor should the index that you use fall outside the jump table. Similarly, control
should only jump to actual instructions, like iinc, not to data values like the 2 in byte 41. In general, the compiler should prevent this.
If it does not, the classfile verifier should detect the problem and refuse to run the program.
lookupswitch
A tableswitch jump table can use a lot of extra space needlessly. Consider the lswitch() method:
int j = 0;
switch (i) {
case 0:
j+= 8;
case -121:
j -= 78;
case 236:
j /= 2;
case 342:
j *= 87;
case 5:
j -= 5;
default:
}
return j;
}
Because the lowest value is -121 and the highest value is 342, compiling this with tableswitch would produce a jump table (342 - -
121 + 1)*4 or 1856 bytes long for only five cases. This clearly is inefficient. Instead, a match-offset table is used. Here’s how it’s set
up.
Immediately following the padding bytes are four bytes that make up a signed int called default. This has the same meaning as it
does for tableswitch; that is, if the value popped from the stack does not match any of the cases, then it should add this int to the
address of the lookupswitch instruction and jump to the resulting address.
The four bytes after default are a signed int called npairs. This is the number of cases stored in the match-offset table and should be a
positive number.
Each entry in the match-offset table is composed of two 4-byte ints. The first int is the value to be matched; the second int is the
offset from the lookupswitch instruction to jump to. Thus, if npairs is 5, as in the above example, then there are 5*4*2=40 bytes in
the match offset table. This saves 1,816 bytes compared with using a jump table, quite a substantial savings. The entries in the match-
offset table are sorted by the values to match. They are not necessarily in the order in which they appear in the source code. The
lswitch() method listed earlier compiles into the byte codes in Listing 5-4.
The lookup switch instruction falls on byte 3. Again, no padding is needed. The default value is in bytes 4 through 7 and is equal to
67. Therefore, when the default case is selected, control jumps to byte 70. Bytes 8 through 11 are the number of pairs, in this
example 5-. The match-offset pairs themselves are in bytes 12 through 51. Notice that the first pair matches the value -121 (bytes 12
through 15) and jumps to instruction 55, 52 bytes after instruction 3 (bytes 16 through 19). The other four pairs are similar.
Notice also that the matches in the byte code have been ordered as -121, 0, 5, 236, 342, even though the source code placed them out
of order as 0, -121, 236, 342, 5-. On the other hand, the actions they take are in the same order in which they appeared in the source
code.
0 iconst_0
1 istore_2
2 iload_1
3 lookupswitch
4 0
5 0
6 0
7 67
8 0
9 0
10 0
11 5
12 -1
13 -1
14 -1
15 -121
16 0
17 0
18 0
19 52
20 0
21 0
22 0
23 0
24 0
25 0
26 0
27 49
28 0
29 0
30 0
31 5
32 0
33 0
34 0
35 64
36 0
37 0
38 0
39 -20
40 0
41 0
42 0
43 55
44 0
45 0
46 1
47 86
48 0
49 0
50 0
51 59
52 iinc
53 2
54 8
55 iinc
56 2
57 -78
58 iload_2
59 iconst_2
60 idiv
61 istore_2
62 iload_2
63 bipush
64 87
65 imul
66 istore_2
67 iinc
68 2
69 -5
70 iload_2
71 ireturn
Objects
Until now, this chapter has considered methods as self-contained entities. However, in real programs, methods call other methods.
They use fields in the object to which they belong and in other objects to which they possess references. They can create new objects
and access the methods and fields of those objects. All of this can be accomplished with only nine new instructions.
In general, all of these instructions operate on or return a reference value. Recall that a reference is a 32-bit object identifier for an
object.
All instance methods have a reference to the object to which they belong stored in the zeroth position of the local variable array. This
enables them to call other methods and to refer to fields in their own object. Static methods do not belong to a particular object, so
they do not have such a reference in their local variable array.
These instructions make frequent use of items in the constant pool. Keep in mind that items in the constant pool may themselves
refer to other items in the constant pool.
Fields
The getfield instruction pops a reference from the stack. This reference is used as an index into the constant pool to get a FieldRef
out of the pool. The FieldRef is used by the virtual machine to find the appropriate field and put its value on the stack.
For example, consider the simple class in Listing 5-5. It compiles to the byte codes shown in Listing 5-6.
int i;
return i;
this.i = i;
int i;
0 aload_0
1 getfield
2 0
3 4
4 ireturn
0 aload_0
1 iload_1
2 putfield
3 0
4 4
5 return
0 aload_0
1 invokespecial
2 0
3 3
4 return
The first instruction in both the getField() and setField() methods is aload_0. This loads the reference from the zeroth local variable
onto the stack. In a non-static method, this is always a reference to the current object.
The next instruction in the getField() method is getfield. This pushes the value of the fourth entry in the constant pool onto the stack.
There’s not enough information here yet to tell what that entry is. I’ll soon revise the Disassembler class to make it more obvious
exactly what that entry is. Then the value is returned.
The setField() method also begins by pushing a reference to the current object onto the stack with aload_0. Then the first argument to
the method, local variable 1, is pushed onto the stack. The putfield instruction pops this value from the stack and puts it in the field
referred to by constant pool entry 4. Again, you don’t know exactly what that field is from the byte code alone. You also need to look
at the constant pool.
Note: There’s a third method in the byte code for the FieldExample class: public void <init>(). It’s more than a little
disconcerting because there was no <init> method in the source code. In fact, <init> isn’t even a legal Java identifier
because of the angle brackets.
<init> is a constructor. In .java byte code, all constructors are named <init> rather than the name of the class as in .java
source code. Furthermore, all classes have at least one constructor, by default a constructor with no arguments, even if the
source code does not. Constructors are discussed in more detail later in this chapter.
Static fields are similar except that the getstatic and putstatic instructions are used instead of getfield and putfield. The two bytes
following the instruction are an index into the constant pool. That entry in the constant pool should contain a FieldRef structure.
With getstatic, the value of that field is pushed onto the static. With putstatic, a value is popped from the stack and stored in that field.
Methods
There are four instructions in Java to call methods: invokevirtual, invokespecial, invokeinterface, and invokestatic. Invokevirtual is
used for normal method calls. The invokeinterface instruction is used to call methods defined in interfaces. The invokestatic
instruction is used to call static methods. Finally, the invokespecial instruction is used to call methods in the superclasses of the
current object.
Although these instructions differ in the kinds of methods they invoke, they all behave similarly. Each reads the next two bytes in the
code array as an index into the constant pool. That pool entry is a Methodref. The Methodref is inspected to find out what arguments
the method takes. These are popped from the stack and placed in the local variable array of the invoked method. Then control moves
to the invoked method. If the invoked method returns a value, that value is pushed onto the stack of the current method.
For example, consider the disassembly of the HelloWorld program in Listing 5-7. The main() method first gets the seventh static
field from the constant pool. The seventh static field in the constant pool is another index into the constant pool, 4. The fourth entry
in the constant pool is still another index into the constant pool, 28. Finally, the 28th entry in the constant pool is the static field you
want, java.lang.System. A reference to this static object is placed on the operand stack.
Next, the ldc instruction pushes the first item in the constant pool onto the stack. The first item in the pool is a string, the UTF8 value
of which is stored at position 29. Therefore, a new string object is created with the value at position 29 in the pool “Hello World!” A
reference to this string is pushed onto the stack.
Now the invokevirtual instruction calls the method referenced by entry 6 in the constant pool. Entry 6 in the constant pool has
ClassIndex 3 and NameAndTypeIndex 9. These are other entries in the constant pool. Entry 3 points to the UTF8 at entry 14, which
is java/io/PrintStream. This tells you that a method of a java.io.PrintStream object is to be invoked. The NameAndTypeIndex points
to entry 9. There you see a NameAndType entry with a NameIndex of 12 and a DescriptorIndex of 21. Entry 12 is println so the
specific method being called is println(). Entry 21 is (Ljava/lang/String;)V, which tells you that the method takes a single string as an
argument and returns void.
import java.io.PrintStream;
0 getstatic
1 0
2 7
3 ldc
4 1
5 invokevirtual
6 0
7 6
8 return
0 aload_0
1 invokespecial
2 0
3 8
4 return
}
}
/*
1: String 29
2: ClassInfo 30
3: ClassInfo 14
4: ClassInfo 28
5: ClassInfo 22
6: MethodRef ClassIndex: 3; NameAndTypeIndex: 9
7: FieldRef ClassIndex: 4; NameAndTypeIndex: 10
8: MethodRef ClassIndex: 5; NameAndTypeIndex: 11
9: NameAndType NameIndex: 12; DescriptorIndex: 21
10: NameAndType NameIndex: 20; DescriptorIndex: 27
11: NameAndType NameIndex: 26; DescriptorIndex: 31
12: UTF8 println
13: UTF8 ConstantValue
14: UTF8 java/io/PrintStream
15: UTF8 Exceptions
16: UTF8 LineNumberTable
17: UTF8 SourceFile
18: UTF8 LocalVariables
19: UTF8 Code
20: UTF8 out
21: UTF8 (Ljava/lang/String;)V
22: UTF8 java/lang/Object
23: UTF8 main
24: UTF8 HelloWorld.java
25: UTF8 ([Ljava/lang/String;)V
26: UTF8 <init>
27: UTF8 Ljava/io/PrintStream;
28: UTF8 java/lang/System
29: UTF8 Hello World!
30: UTF8 HelloWorld
31: UTF8 ()V
*/
The init method also uses an invoke instruction:, invokespecial. This invokes the method described by entry 8 in the constant pool.
Entry 8 has a ClassIndex of five and a NameAndTypeIndex of 11. Looking at entry 5, you are referred to the UTF8 at entry 22, java/
lang/Object. You are therefore invoking a method in java.lang.Object. Looking at entry 11, you’re referred to the NameIndex at entry
26 and the DescriptorIndex at entry 31. Entry 26 is <init> and entry 31 is ()V. Therefore, the invokespecial instruction is invoking
the noargs constructor from java.lang.Object. Remember that the first action of every constructor is to invoke a constructor for the
superclass.
The invokestatic and invokeinterface instructions behave exactly the same way. The only difference is that invokestatic invokes
static methods and invokeinterface invokes methods from interfaces.
Returning values from methods is straightforward. You can return an int, a float, a long, a double, a reference, or nothing at all. In
byte code, the last line of each method will be one of the six return instructions. A value of the appropriate type is popped from the
stack and pushed onto the top of the stack of the calling method. After that, all data for this method call is disposed of. The six return
instructions are
You’ve seen examples of these instructions at the end of every complete disassembled method in this chapter. Even if there’s no
return statement in the source code, the compiler inserts a generic return statement at the end of each method.
Now that you know how to work with fields and methods in Java byte code, the only object instruction left is the creation of new
objects. There is a single instruction to create new objects, the appropriately named new instruction. Listing 5-8 is a very simple class
that does one thing: create a new java.lang.Integer object with the value 7.
class newTest {
Listing 5-9 shows the byte code for this example. Notice the new instruction at byte 0 of the makeInteger() method. This is followed
by the two bytes of a signed short with the value 3. If you guessed that this 3 is an index into the constant pool, you are correct.
Entry 3 in the constant pool is a ClassInfo structure that points to the UTF8 structure in entry 19 of the constant pool. That entry is
java/lang/Integer, and it tells the new instruction to create a new object of type java.lang.Integer.
The new instruction only allocates space for the object in the virtual machine. It does not initialize it. The constructor still needs to be
called. Bytes 3 and 4 push the argument for the constructor onto the stack. Bytes 5 through 7 invoke the constructor with the
invokespecial instruction. Finally, byte 8 returns void.
0 new
1 0
2 3
3 bipush
4 7
5 invokespecial
6 0
7 5
8 return
}
void <init>() {
0 aload_0
1 invokespecial
2 0
3 4
4 return
/*
1: ClassInfo 20
2: ClassInfo 22
3: ClassInfo 19
4: MethodRef ClassIndex: 1; NameAndTypeIndex: 7
5: MethodRef ClassIndex: 3; NameAndTypeIndex: 6
6: NameAndType NameIndex: 21; DescriptorIndex: 9
7: NameAndType NameIndex: 21; DescriptorIndex: 24
8: UTF8 this
9: UTF8 (I)V
10: UTF8 newTest.java
11: UTF8 ConstantValue
12: UTF8 LocalVariableTable
13: UTF8 Exceptions
14: UTF8 LineNumberTable
15: UTF8 SourceFile
16: UTF8 LocalVariables
17: UTF8 Code
18: UTF8 makeInteger
19: UTF8 java/lang/Integer
20: UTF8 java/lang/Object
21: UTF8 <init>
22: UTF8 newTest
23: UTF8 LnewTest;
24: UTF8 ()V
*/
Arrays
In Java, arrays are objects and array variables are references. However, there are four byte codes that operate specifically on arrays.
The newarray instruction allocates new arrays of primitive types such as int and double. The anewarray instruction allocates new
arrays of references. The multianewarray instruction allocates multidimensional arrays of any type. The arraylength instruction
returns the length of an array. The iaload, laload, faload, daload, aaload, baload, caload, and saload instructions copy values from an
array onto a stack. The iastore, lastore, fastore, dastore, aastore, bastore, castore, and satore instructions pop values from the stack
and put them in an array.
Creating arrays
Java must allocate the right amount of space in the heap for an array of seven ints. To do this, it first pushes the value 7 onto the
stack. Then it uses the newarray instruction to actually allocate the space. The number of ints to allocate is popped from the stack.
The type of the array to allocate is read from the next byte in the code array. An int array is type 11. A reference to the array is
pushed onto the stack. This reference is then popped from the stack and copied into whichever position e occupies in the local
variable array. Thus, the byte code would look something like this:
bipush
7
newarray
11
astore_1
The newarray instruction knows the sizes of the different types it can hold and allocates the appropriate amount of space.
Code Type
4 boolean
5 char
6 float
7 double
8 byte
9 short
10 int
11 long
Arrays are one of the few areas in Java in which all primitive data types are supported equally. Short arrays really are arrays of shorts
and not just a subset of ints. Although the difference in storage requirements between a single short variable and a single int variable
is trivial on modern computers, the difference between a large array of shorts and a large array of ints can still be significant.
anewarray
Arrays of objects are really arrays of references. The type of such an array is given as a 2-byte index into the constant pool that
follows the anewarray instruction. Like the newarray instruction, the length of the array is popped from the stack, and a reference to
the new array is pushed onto the stack. Thus,
0 bipush
1 7
2 anewarray
3 0
4 3
5 astore_1
...
/* Constant Pool
1: ClassInfo 21
2: ClassInfo 23
3: ClassInfo 20
...
20: UTF8 java/lang/Integer
...
The difference between newarray and anewarray is that newarray encodes the type of the data directly into the code array, whereas
anewarray needs to refer to the constant pool.
Previous Table of Contents Next
Java Secrets
by Elliotte Rusty Harold
IDG Books, IDG Books Worldwide, Inc.
ISBN: 0764580078 Pub
Date: 05/01/97 Buy It
multianewarray
The multianewarray instruction creates multidimensional arrays of both primitive and reference types. The multianewarray
instruction is similar to the anewarray instruction. The type of the array is read from the constant pool. The index into the constant
pool is a 2-byte short that immediately follows the multianewarray instruction. Following this is an unsigned byte that contains the
number of dimensions in the array.
Multidimensional arrays of primitive types such as int or double are listed in the constant pool as a series of left brackets, one for
each dimension, followed by a letter for the type. Thus, a two-dimensional array of ints (int[][]) is listed as [[I, and a three-
dimensional array of doubles (double[][][]) is listed as [[[D. Table 5-4 lists the abbreviations for the different primitive data types.
Table 5-4Abbreviations for the primitive data types used in the constant pool
Abbreviation Type
B byte
C char
D double
F float
I int
J long
S short
Z boolean
...
0 bipush
1 7
2 bipush
3 6
4 multianewarray
5 0
6 3
7 2
8 astore_1
...
/* Constant Pool
1: ClassInfo 16
2: ClassInfo 19
3: ClassInfo 17
...
17: UTF8 [[I
...
It’s not enough just to create arrays. You also have to be able to put values in the arrays (storing) and get values out of the arrays
(loading).
Loading
There are eight instructions to copy values from an array onto the operand stack, one for each primitive data type except boolean and
one for reference types. These instructions operate purely on the stack. Each pops the index of the component to load from the array
and a reference to the array from the stack. Then, the component at that index in the array is pushed onto the stack. Figure 5-43
demonstrates with the iaload instruction:
The aaload, saload, caload, and faload instructions behave identically, except that they operate on arrays of type reference, short,
char, and float, respectively, and push those types onto the stack. As usual, shorts and chars are zero-extended to ints before being
pushed onto the stack. The baload instruction does double duty for loading both bytes and booleans. The result is sign-extended to an
int. Otherwise, baload behaves exactly like iaload.
The daload and laload instructions copy doubles and longs from an array onto the stack. Of course, each requires two words on the
stack for the result. Figure 5-44 demonstrates with the daload instruction.
Storing
There are eight instructions to pop values from the operand stack and store them in an array, one for each primitive data type except
boolean and one for reference types. These instructions operate purely on the stack. Each pops three values from the stack, the value
to be placed in the array, the index at which to place it, and a reference to the array in that order. Nothing is pushed back onto the
stack. Then the value is stored in at that index in the array. Figure 5-45 demonstrates with the iastore instruction.
The aastore and fastore instructions behave identically, except that they operate on arrays of type reference and float respectively and
pop those types onto the stack.
The sastore and castore instructions pop a 4-byte int from the stack, lop off the high-order two bytes, and store the result in the
referenced array. As long as the value actually fits in a short or a char, this is transparent and works exactly as does the iastore
instruction. However, as discussed in Chapter 2, you encounter problems with int values above 32,767 or below -32,768.
The bastore instruction stores both bytes and booleans. It pops a 4-byte int, an index, and a reference to an array from the stack,
truncates the int to 1-byte, and stores the truncated result at the indexed position in the referenced array.
The dastore and lastore pop doubles and longs from the stack and store them in an array. These instructions pop four words from the
stack: the low-order four bytes of the value, the high-order four bytes of the value, the index into the array, and a reference to an
array. Figure 5-46 demonstrates with the lastore instruction.
arraylength
The arraylength instruction pops a reference to an array from the stack, checks the length of the array, and pushes the result back
onto the stack. This instruction is used when you access the length “member” of an array. For example, it’s common to loop through
all the command line arguments to main like this:
The length of the args array is taken by pushing a reference to args onto the stack, executing the arraylength instruction, and then
popping the result. Here’s the byte code:
0 iconst_0
1 istore_1
2 goto
3 0
4 6
5 iinc
6 1
7 1
8 iload_1
9 aload_0
10 arraylength
11 if_icmplt
12 -1
13 -6
14 return
Because main() is a static method, the zeroth component of the local variable array is the first argument to the method, not a
reference to the current object. In other words, it’s a reference to the args array. Thus, byte 9 — aload_0 — pushes a reference to
args onto the operand stack. Byte 10 — arraylength — pops that reference from the stack and pushes the length of the array onto the
stack.
Exceptions
Exceptions are implemented mostly through the opcodes that you’ve already encountered, such as new, astore, and goto. The one
new opcode that you need to throw exceptions is athrow. The opcode athrow pops a reference to a Throwable object (an instance of a
subclass of java.lang.Error or java.lang.Exception) from the stack and then searches the current frame for the nearest catch clause
that catches that type of Throwable.
If a catch clause is found that catches this exception, then control moves into the catch clause and program execution resumes there.
If, on the other hand, no catch clause is found that can handle this type of exception or error, then the current thread terminates.
Catch clauses do not require any special opcodes. They are just different areas of the byte code for a method. The ExceptionsTable
attribute for a method stores the addresses of the catch clauses that handle different classes of ex