Java Pitfalls All Versions
This draft deletes the entire topic.
Examples
-
(This pitfall applies equally to all primitive wrapper types, but we will illustrate it for
Integer
andint
.)When working with
Integer
objects, it is tempting to use==
to compare values, because that is what you would do withint
values. And in some cases this will seem to work:Integer int1_1 = Integer.valueOf("1"); Integer int1_2 = Integer.valueOf(1); System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2)); // true System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2)); // true
Here we created two
Integer
objects with the value1
and compare them (In this case we created one from aString
and one from anint
literal. There are other alternatives). Also, we observe that the two comparison methods (==
andequals
) both yieldtrue
.This behavior changes when we choose different values:
Integer int2_1 = Integer.valueOf("1000"); Integer int2_2 = Integer.valueOf(1000); System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2)); // false System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2)); // true
In this case, only the
equals
comparison yields the correct result.The reason for this difference in behavior is, that the JVM maintains a cache of
Integer
objects for the range -128 to 127. (The upper value can be overridden with the system property "java.lang.Integer.IntegerCache.high"). For values in this range, theInteger.valueOf()
will return the cached value rather than creating a new one.Thus, in the first example the
Integer.valueOf(1)
andInteger.valueOf("1")
calls returned the same cachedInteger
instance. By contrast, in the second example theInteger.valueOf(1000)
andInteger.valueOf("1000")
both created and returned newInteger
objects.The
==
operator for reference types tests for reference equality (i.e. the same object). Therefore, in the first exampleint1_1 == int2_1
istrue
because the references are the same. In the second exampleint2_1 == int2_2
is false because the references are different. -
A common mistake for Java beginners is to use the
==
operator to test if two strings are equal. For example:public class Hello { public static void main(String[] args) { if (args.length > 0) { if (args[0] == "hello") { System.out.println("Hello back to you"); } else { System.out.println("Are you feeling grumpy today?"); } } } }
The above program is supposed to test the first command line argument and print different messages when it and isn't the word "hello". But the problem is that it won't work. That program will output "Are you feeling grumpy today?" no matter what the first command line argument is.
When you use
==
to test strings, what you are actually testing is if twoString
objects are the same Java object. Unfortunately, that is not what string equality means in Java. In fact, the correct way to test strings is to use theequals(Object)
method. For a pair of strings, that will test to see if they consist of the same characters in the same order ... which is what we usually want.public class Hello2 { public static void main(String[] args) { if (args.length > 0) { if (args[0].equals("hello")) { System.out.println("Hello back to you"); } else { System.out.println("Are you feeling grumpy today?"); } } } }
But it actually gets worse. The problem is that
==
will give the expected answer in some circumstances. For examplepublic class Test1 { public static void main(String[] args) { String s1 = "hello"; String s2 = "hello"; if (s1 == s2) { System.out.println("same"); } else { System.out.println("different"); } } }
Surprisingly (perhaps), this will print "same", even though we are testing the strings the wrong way. Why is that? Because the Java Language Specification (Section 3.10.5: String Literals) stipulates that any two string >>literals<< consisting of the same characters will actually be represented by the same Java object. Hence, the
==
test will give true for equal literals. (The string literals are "interned" and added to a shared "string pool" when your code is loaded ... but that is actually an implementation detail.)To add to the confusion, the Java Language Specification also stipulates that when you have a compile-time constant expression that concatenates two string literals, that is equivalent to a single literal. Thus:
public class Test1 { public static void main(String[] args) { String s1 = "hello"; String s2 = "hel" + "lo"; String s3 = " mum"; if (s1 == s2) { System.out.println("1. same"); } else { System.out.println("1. different"); } if (s1 + s3 == "hello mum") { System.out.println("2. same"); } else { System.out.println("2. different"); } } }
This will output "1. same" and "2. different". In the first case, the
+
expression is evaluated at compile time and we compare oneString
object with itself. In the second case, it is evaluated at run time and we compare two differentString
objectsIn summary, using
==
to test strings in Java is almost always incorrect, but it is not guaranteed to give the wrong answer. -
-
Every time a program opens a resource, such as a file or network connection, it is important to free the resource once you are done using it. Similar caution should be taken if any exception were to be thrown during operations on such resources. One could argue that the
FileInputStream
has a finalizer that invokes theclose()
method on a garbage collection event; however, since we can’t be sure when a garbage collection cycle will start, the input stream can consume computer resources for an indefinite period of time. The resource must be closed in afinally
section of a try-catch block:Java SE 7private static void printFileJava6() throws IOException { FileInputStream input; try { input = new FileInputStream("file.txt"); int data = input.read(); while (data != -1){ System.out.print((char) data); data = input.read(); } } finally { if (input != null) { input.close(); } } }
Since Java 7 there is a really useful and neat statement introduced in Java 7 particularly for this case, called try-with-resources:
Java SE 7private static void printFileJava7() throws IOException { try (FileInputStream input = new FileInputStream("file.txt")) { int data = input.read(); while (data != -1){ System.out.print((char) data); data = input.read(); } } }
The try-with-resources statement can be used with any object that implements the
Closeable
orAutoClosable
interface. It ensures that each resource is closed by the end of the statement. The difference between the two interfaces is, that theclose()
method ofCloseable
throws anIOException
which has to be handled in some way.In cases, where the resource is opened before but should the safely be closed after use, one can assign it to a local variable inside the the try-with-resources
Java SE 7private static void printFileJava7(InputStream extResource) throws IOException { try (InputStream input = extResource) { ... //access resource } }
The local resource variable created in the try-with-resources constructor is effectively final.
-
Some people recommend that you should apply various tests to a file before attempting to open it. For example, this method attempts to check if a path corresponds to a file that can be read:
public static File getValidatedFile(String path) throws IOException { File f = new File(path); if (!f.exists()) throw new IOException("Error: not found: " + path); if (!f.isFile()) throw new IOException("Error: Is a directory: " + path); if (!f.canRead()) throw new IOException("Error: cannot read file: " + path); return f; }
You might use the above method like this:
File f = null; try { f = getValidatedFile("somefile"); } catch (IOException ex) { System.err.println(ex.getMessage()); return; } try (InputStream is = new FileInputStream(file)) { // Read data etc. }
The first problem is that signature for
FileInputStream(File)
means that the compiler will still insist that we catchIOException
here, or further up the stack.The second problem is that checks performed by
getValidatedFile
do not guarantee that theFileInputStream
will succeed.-
Race conditions: another thread or a separate process could rename or delete the file, or remove read access after the
getValidatedFile
returns. That would lead to a "plain"IOException
without the custom message. -
There are edge cases not covered by those tests. For example, on a system with SELinux in "enforcing" mode, an attempt to read a file can fail despite
canRead()
returningtrue
.
The third problem is that the tests are inefficient. For example, the
exists
,isFile
andcanRead
calls will result in 3 syscalls to perform the respective checks. Then when the file is opened, another syscall is made which has to check the same checks all over again.In short, methods like
getValidatedFile
are misguided. It is better to simply attempt to open the file and handle the exception:try (InputStream is = new FileInputStream("somefile")) { // Read data etc. } catch (IOException ex) { System.err.println("IO Error processing 'somefile': " + ex.getMessage()); return; }
If you wanted to distinguish IO errors thrown while opening and reading, you could use a nested try / catch. If you wanted to produce better diagnostics for open failures, you could perform the
exists
,isFile
andcanRead
checks in the handler. -
-
No Java variable represents an object.
String foo; // NOT AN OBJECT
Neither does any Java array contain objects.
String bar[] = new String[100]; // No member is an object.
If you mistakenly think of variables as objects, the actual behavior of the Java language will surprise you.
-
For Java variables which have a primitive type (such as
int
orfloat
) the variable holds a copy of the value. All copies of a primitive value are indistinguishable; i.e. there is only oneint
value for the number one. Primitive values are not objects and they do not behave like objects. -
For Java variables which have a reference type (either a class or an array type) the variable holds a reference. All copies of a reference are indistinguishable. References may point to objects, or they may be
null
which means that they point to no object. However, they are not objects and they don't behave like objects.
Variables are not objects in either case, and they don't contain objects in either case. They may contain references to objects, but that is saying something different.
Example class
The examples that follow use this class, which represents a point in 2D space.
public final class MutableLocation { public int x; public int y; public MutableLocation(int x, int y) { this.x = x; this.y = y; } public boolean equals(Object other) { if (!(other instanceof MutableLocation) { return false; } MutableLocation that = (MutableLocation) other; return this.x = that.x && this.y == that.y; } }
An instance of this class is an object that has two fields
x
andy
which have the typeint
.We can have many instances of the
MutableLocation
class. Some will represent the same locations in 2D space; i.e. the respective values ofx
andy
will match. Others will represent different locations.Multiple variables can point to the same object
MutableLocation here = new MutableLocation(1, 2); MutableLocation there = here; MutableLocation elsewhere = new MutableLocation(1, 2);
In the above, we have declared three variables
here
,there
andelsewhere
that can hold references toMutableLocation
objects.If you (incorrectly) think of these variables as being objects, then you are likely to misread the statements as saying:
- Copy the location "[1, 2]" to
here
- Copy the location "[1, 2]" to
there
- Copy the location "[1, 2]" to
elsewhere
From that, you are likely to infer we have three independent objects in the three variables. In fact there are only two objects created by the above. The variables
here
andthere
actually refer to the same object.We can demonstrate this. Assuming the variable declarations as above:
System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x + "elsewhere.x is " + elsewhere.x); here.x = 42; System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x + "elsewhere.x is " + elsewhere.x);
This will output the following:
BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1 AFTER: here.x is 42, there.x is 42, elsewhere.x is 1
We assigned a new value to
here.x
and it changed the value that we see viathere.x
. They are referring to the same object. But the value that we see viaelsewhere.x
has not changed, soelsewhere
must refer to a different object.If a variable was an object, then the assignment
here.x = 42
would not changethere.x
.The equality operator does NOT test that two objects are equal
Applying the equality (
==
) operator to reference values tests if the values refer to the same object. It does not test whether two (different) objects are "equal" in the intuitive sense.MutableLocation here = new MutableLocation(1, 2); MutableLocation there = here; MutableLocation elsewhere = new MutableLocation(1, 2); if (here == there) { System.out.println("here is there"); } if (here == elsewhere) { System.out.println("here is elsewhere"); }
This will print "here is there", but it won't print "here is elsewhere". (The references in
here
andelsewhere
are for two distinct objects.)By contrast, if we call the
equals(Object)
method that we implemented above, we are going to test if twoMutableLocation
instances have an equal location.if (here.equals(there)) { System.out.println("here equals there"); } if (here.equals(elsewhere)) { System.out.println("here equals elsewhere"); }
This will print both messages. In particular,
here.equals(elsewhere)
returnstrue
because the semantic criteria we chose for equality of twoMutableLocation
objects has been satisfied.Method calls do NOT pass objects at all
Java method calls use pass by value1 to pass arguments and return a result.
When you pass a reference value to a method, you're actually passing a reference to an object by value, which means that it is creating a copy of the object reference.
As long as both object references are still pointing to the same object, you can modify that object from either reference, and this is what causes confusion for some.
However, you are not passing an object by reference2. The distinction is that if the object reference copy is modified to point to another object, the original object reference will still point to the original object.
void f(MutableLocation foo) { foo = new MutableLocation(3, 4); // Point local foo at a different object. } void g() { MutableLocation foo = MutableLocation(1, 2); f(foo); System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1". }
Neither are you passing a copy of the object.
void f(MutableLocation foo) { foo.x = 42; } void g() { MutableLocation foo = new MutableLocation(0, 0); f(foo); System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42" }
1 - In languages like Python and Ruby, the term "pass by sharing" is preferred for "pass by value" of an object / reference.
2 - The term "pass by reference" or "call by reference" has a very specific meaning in programming language terminology. In effect, it means that you pass the address of a variable or an array element, so that when the called method assigns a new value to the formal argument, it changes the value in the original variable. Java does not support this. For a more fulsome description of different mechanisms for passing parameters, please refer to https://en.wikipedia.org/wiki/Evaluation_strategy.
-
-
This exception occurs when a collection is modified while iterating over it using methods other than those provided by the iterator object. For example, we have a list of hats and we want to remove all those that have ear flaps:
List<IHat> hats = new ArrayList<>(); hats.add(new Ushanka()); // that one has ear flaps hats.add(new Fedora()); hats.add(new Sombrero()); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hats.remove(hat); } }
If we run this code, ConcurrentModificationException will be raised since the code modifies the collection while iterating it. The same exception may occur if one of the multiple threads working with the same list is trying to modify the collection while others iterate over it. Concurrent modification of collections in multiple threads is a natural thing, but should be treated with usual tools from the concurrent programming toolbox such as synchronization locks, special collections adopted for concurrent modification, modifying the cloned collection from initial etc.
-
Java uses automatic memory management, and while it’s a relief to forget about allocating and freeing memory manually, it doesn’t mean that a beginning Java developer should not be aware of how memory is used in the application.
Problems with memory allocations are still possible. As long as a program creates references to objects that are not needed anymore, it will not be freed. In a way, we can still call this memory leak. Memory leaks in Java can happen in various ways, but the most common reason is everlasting object references, because the garbage collector can’t remove objects from the heap while there are still references to them. One can create such a reference by defining class with a static field containing some collection of objects, and forgetting to set that static field to null after the collection is no longer needed. Static fields are considered
GC
roots and are never collected.Another potential reason behind such memory leaks is a group of objects referencing each other, causing circular dependencies so that the garbage collector can’t decide whether these objects with cross-dependency references are needed or not. Another issue is leaks in non-heap memory when JNI is used.
The primitive leak example could look like the following:
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>(); final BigDecimal divisor = new BigDecimal(51); scheduledExecutorService.scheduleAtFixedRate(() -> { BigDecimal number = numbers.peekLast(); if (number != null && number.remainder(divisor).byteValue() == 0) { System.out.println("Number: " + number); System.out.println("Deque size: " + numbers.size()); } }, 10, 10, TimeUnit.MILLISECONDS); scheduledExecutorService.scheduleAtFixedRate(() -> { numbers.add(new BigDecimal(System.currentTimeMillis())); }, 10, 10, TimeUnit.MILLISECONDS); try { scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); }
This example creates two scheduled tasks. The first task takes the last number from a deque called “numbers” and prints the number and deque size in case the number is divisible by 51. The second task puts numbers into the deque. Both tasks are scheduled at a fixed rate, and run every 10 ms.
If the code is executed, you’ll see that the size of the deque is permanently increasing. This will eventually cause the deque to be filled with objects consuming all available heap memory.
To prevent this while preserving the semantics of this program, we can use a different method for taking numbers from the deque: “pollLast”. Contrary to the method “peekLast”, “pollLast” returns the element and removes it from the deque while “peekLast” only returns the last element.
-
The
Runtime.exec(String ...)
andRuntime.exec(String)
methods allow you to execute a command as an external process1. In the first version, you supply the command name and the command arguments as separate elements of the string array, and the Java runtime requests the OS runtime system to start the external command. The second version is deceptively to use, but it has some hidden pit falls.First of all, here is an example of using
exec(String)
being used safely:Process p = Runtime.exec("mkdir /tmp/testDir"); p.waitFor(); if (p.exitValue() == 0) { System.out.println("created the directory"); }
Spaces in pathnames
Suppose that we generalize the example above so that we can create an arbitrary directory:
Process p = Runtime.exec("mkdir " + dirPath); // ...
This will typically work, but it will fail if
dirPath
is (for example) "/home/user/My Documents". The problem is thatexec(String)
splits the string into a command and arguments by simply looking for whitespace. The command string:"mkdir /home/user/My Documents"
will be split into:
"mkdir", "/home/user/My", "Documents"
and this will cause the "mkdir" command to fail because it expects one argument, not two.
Faced with this, some programmers try to add quotes around the pathname. This doesn't work either:
"mkdir \"/home/user/My Documents\""
will be split into:
"mkdir", "\"/home/user/My", "Documents\""
The extra double-quote characters that were added in attempt to "quote" the spaces are treated like any other non-whitespace characters. Indeed, anything we do quote or escape the spaces is going to fail.
The way to deal with this particular problems is to use the
exec(String ...)
overload.Process p = Runtime.exec("mkdir", dirPath); // ...
This will work if
dirpath
includes whitespace characters because this overload ofexec
does not attempt to split the arguments. The strings are passed through to the OSexec
system call as-is.Redirection, pipelines and other shell syntax
Suppose that we want to redirect an external command's input or output, or run a pipeline. For example:
Process p = Runtime.exec("find / -name *.java -print 2>/dev/null");
or
Process p = Runtime.exec("find source -name *.java | xargs grep package");
(The first example lists the names of all Java files in the file system, and the second one prints the
package
statements2 in the Java files in the "source" tree.)These are not going to work as expected. In the first case, the "find" command will be run with "2>/dev/null" as a command argument. It will not be interpreted as a redirection. In the second example, the pipe character ("|") and the works following it will be given to the "find" command.
The problem here is that the
exec
methods andProcessBuilder
do not understand any shell syntax. This includes redirections, pipelines, variable expansion, globbing, and so on.In a few cases (for example, simple redirection) you can easily achieve the desired effect using
ProcessBuilder
. However, this is not true in general. An alternative approach is to run the command line in a shell; for example:Process p = Runtime.exec("bash", "-c", "find / -name *.java -print 2>/dev/null");
or
Process p = Runtime.exec("bash", "-c", "find source -name \\*.java | xargs grep package");
But note that in the second example, we needed to escape the wildcard character ("*") because we want the wildcard to be interpreted by "find" rather than the shell.
Shell builtin commands don't work
Suppose the following examples won't work on a system with a UNIX-like shell:
Process p = Runtime.exec("cd", "/tmp"); // Change java app's home directory
or
Process p = Runtime.exec("export", "NAME=value"); // Export NAME to the java app's environment
There are a couple of reasons why this won't work:
-
On "cd" and "export" commands are shell builtin commands. They don't exist as distinct executables.
-
For shell builtins to do what they are supposed to do (e.g. change the working directory, update the environment), they need to change the place where that state resides. For a normal application (including a Java application) the state is associated with the application process. So for example, the child process that would run the "cd" command could not change the working directory of its parent "java" process. Similarly, one
exec
'd process cannot change the working directory for a process that follows it.
This reasoning applies to all shell builtin commands.
1 - You can use
ProcessBuilder
as well, but that is not relevant to the point of this example.2 - This is a bit rough and ready ... but once again, the failings of this approach are not relevant to the example.
-
I am downvoting this example because it is...
Topic Outline
- Pitfall: using == to compare primitive wrappers objects such as Integer
- Pitfall: using == to compare strings
- Pitfall: forgetting to free resources
- Pitfall: testing a file before attempting to open it.
- Pitfall: thinking of variables as objects
- Pitfall: concurrent modification exceptions
- Pitfall: memory leaks
- Pitfall: Runtime.exec, Process and ProcessBuilder don't understand shell syntax
Sign up or log in
Save edit as a guest
Join Stack Overflow
Using Google
Using Facebook
Using Email and Password
We recognize you from another Stack Exchange Network site!
Join and Save Draft