Some small Java code examples for testing your Java knowledge.
At work we noticed that some developers didn't fully understand some of the Java basics they were using day in day out. So we came up with some code examples they could use to check their knowledge. We don't use these for job interviews yet.
I'll add some comments and Clojure [1] code. Clojure is a LISP-like functional JVM language. I love Java (and adore Clojure) but I believe it has some properties that make it hard to get right. So I'll say a little about why certain error/bugs are less likely in Clojure than in Java.
import static java.lang.System.*;
class AndOr {
static boolean yep() {
out.println("yep");
return true;
}
static boolean nope() {
out.println("nope");
return false;
}
public static void main(String... args) {
out.println(yep() | nope());
out.println(yep() || nope());
out.println(nope() & yep());
out.println(nope() && yep());
}
}
Build & run:
~/java-quiz$ javac AndOr.java
~/java-quiz$ java AndOr
yep
nope
true
yep
true
nope
yep
false
nope
false
The logical operators ||
and &&
[2] are short-circuit [1]
operators. So they're only evaluated (i.e. their operands are
evaluated) until the overall result is known. The bit-wise operators
|
and &
are always evaluated completely.
Some developers accidentally use the bit-wise operators instead of
the logical operators. The code will compile and run in many cases and
it will even (kind of) work. But (x != null) && (x.foo())
is
different from (x != null) & (x.foo())
.
In Clojure there are no operators (and no operator precedence!). You
use and
[3], or
(both with short-circuit semantics), bit-and
and bit-or
(for integer numbers only though). The names are clear
and explicit. You won't confuse them.
[1] https://en.wikipedia.org/wiki/Short-circuit_evaluation
[2] https://docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html
[3] https://clojuredocs.org/clojure.core/and
import static java.lang.System.*;
import java.util.Arrays;
class ArraySideeffect {
static int[] sort(int[] x) {
Arrays.sort(x);
return x;
}
public static void main(String... args) {
final int[] i = { 3, 2, 1 };
final int[] k = i;
out.println(Arrays.toString(i));
out.println(Arrays.toString(k));
final int[] j = sort(i);
out.println(Arrays.toString(i));
out.println(Arrays.toString(j));
out.println(Arrays.toString(k));
}
}
Build & run:
~/java-quiz$ javac ArraySideeffect.java
~/java-quiz$ java ArraySideeffect
[3, 2, 1]
[3, 2, 1]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
Mutation of state (und thus side effects [1]) is ubiquitous in
Java. Here we first create a state sharing and then sort
the
array. All variables refer to the same mutable array.
Many developers do not understand the difference between the
object/instance (the array in this case) and the reference(s) to such
an object. Java uses by value semantics [3] but this
description/term leads some developers to believe that sort(i)
will
pass the object/value/array to the method and not just the
reference to that object/value/array.
Here we introduce the sort
method to further trick the reader: it
looks like a pure function, but it just returns its argument
(i.e. the reference to the mutable array) -- well, thank's for that!
This kind of error (i.e. introducing side effects via state sharing
between arguments and return values) is done quite often by developers
unintentionally and these errors are hard to find.
In Clojure you almost always use immutable data types (including immutable collections -- called persistent data structures [2]). So there is no danger of side effects. In Clojure state is managed by several mutable reference types. But the values (like lists and maps) are kept immutable. So it's always safe to pass/receive values to/from functions.
A Clojure program that comes close to the structure of the Java program from above looks like this:
(defn main []
(let [i [3 2 1]
k i
_ (println i)
_ (println k)
j (into [] (sort i))]
(println i)
(println j)
(println k)))
And run (you'll need to get clojure.jar
):
~/java-quiz$ java -jar clojure.jar -i array-side-effect.clj -e '(main)'
[3 2 1]
[3 2 1]
[3 2 1]
[1 2 3]
[3 2 1]
So only j
is sorted -- no side effect.
Note: Clojure can use Java's mutable arrays directly and in this case you get uncontrolled state sharing and side effects in Clojure as well [4].
[1] https://en.wikipedia.org/wiki/Side_effect_(computer_science)
[2] https://clojure.org/reference/data_structures
[3] http://javadude.com/articles/passbyvalue.htm
[4] https://clojuredocs.org/clojure.core/sort
So many hard problem are introduced through mutable shared state:
-
side effects
-
the need for defensive copies and cloning
-
race conditions and the need for synchronization in multi-threaded programms (and the need to understand chapter 17 of the JLS [1])
With Java 8 we have functional closures/lamdas/streams: given mutable state all this will lead to more broken code.
[1] https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
import static java.lang.System.*;
class Autoboxing {
public static void main(String... args) {
Integer a = 42, b = 42;
out.println("a == b : " + (a == b));
out.println("a == 42 : " + (a == 42));
out.println("a.equals(b) : " + (a.equals(b)));
Integer c = 666, d = 666;
out.println("c == d : " + (c == d));
out.println("c == 666 : " + (c == 666));
out.println("c.equals(d) : " + (c.equals(d)));
Boolean t = true, s = true;
out.println("t == s : " + (t == s));
out.println("t == true : " + (t == true));
out.println("c.equals(d) : " + (c.equals(d)));
}
}
Build & run:
~/java-quiz$ javac Autoboxing.java
~/java-quiz$ java Autoboxing
a == b : true
a == 42 : true
a.equals(b) : true
c == d : false
c == 666 : true
c.equals(d) : true
t == s : true
t == true : true
c.equals(d) : true
Comparing a == b
and c == d
is done with no auto-unboxing: this
just compares references. a
and b
reference the same cached
object because they are both auto-boxed from 42
via
Integer.valueOf(int)
[1]. So they're identical. On my JVM the
auto-boxed values for 666
are not cached in Integer
so c
and d
reference two different instances.
Comparing a == 42
will auto-unbox a
and compare the two (native)
int
(not Integer
) values.
Autoboxing seems to give the developer the freedom to mix the use of
native types (int
, long
, float
, double
, boolean
) and their
wrapper counterparts (Integer
etc.) arbitrarily and to substitute
one for the other.
But:
-
boxing und unboxing costs time and space -- so you should only do it when you really need it. Some developers will use
for (Integer i = 0; [...])
without knowing what they're doing. -
it introduces the chance to get the comparision wrong (see above) -- in this case your code may work for the first 127 test cases but then no more.
-
Double
/Float
anddouble
/float
have differentequals
and ordering semantics for0.0
/-0.0
andNaN
[2] which may come as a surprise.
In Clojure there is no auto-boxing since you're usually using the wrapper types only (see "Numbers" in [3]). But for Java-interop [4] there are ways to deal with native number types.
[1] https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html#valueOf-int-
[2] https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#equals-java.lang.Object-
[3] https://clojure.org/api/cheatsheet
[4] https://clojure.org/reference/java_interop
import static java.lang.System.*;
import java.math.BigInteger;
class BigIntegerQuiz {
public static void main(String... args) {
BigInteger a = BigInteger.valueOf(Integer.parseInt(args[0]));
a.add(BigInteger.valueOf(Integer.parseInt(args[1])));
out.println(a);
}
}
Build & run:
~/java-quiz$ javac BigIntegerQuiz.java
~/java-quiz$ java BigIntegerQuiz 1 2
1
BigInteger
is an immutable class and BigInteger.add()
is a pure
function and not a mutator (like List.add()
). Readers/Developes may
not know that and misbelieve that BigInteger.add()
changes the
Integer
instance.
In Clojure you use +
, -
, *
and /
as you expect (like in (+ a 1)
). Note that Clojure has built-in literals for BigDecimal
(not
BigInteger
though):
(+ 1.20M 1.4M) ; -> 2.60M
(type 2.60M) ; -> java.math.BigDecimal
import static java.lang.System.*;
class NumberQuiz {
public static void main(String... args) {
out.println("A:" + (0.2 == 0.2f));
out.println("B:" + (0.5 == 0.5f));
float sf = 0.2f + 0.2f + 0.2f;
double sd = 0.2 + 0.2 + 0.2;
out.println("sf:" + sf);
out.println("sd:" + sd);
out.println("C:" + (0.0 == 0.0f));
out.println("D:" + (0.0 == -0.0f));
out.println("E:" + (0.0 == -0.0));
out.println("F:" + new Double(0.0).equals(0.0f));
out.println("G:" + new Double(0.0).equals(-0.0f));
out.println("H:" + new Double(0.0).equals(-0.0));
out.println("I:" + (Double.NaN == Double.NaN));
out.println("J:" + (Double.NaN > 0.0));
out.println("K:" + (Double.NaN < 0.0));
out.println("L:" + (new Double(Double.NaN).equals(Double.NaN)));
out.println("M:" + (new Double(0.0).compareTo(Double.NaN)));
short s = (short) (Short.MAX_VALUE + 1);
out.println("N:" + Short.MAX_VALUE);
out.println("O:" + (Short.MAX_VALUE + 1));
out.println("P:" + s);
out.println("Q:" + Integer.MAX_VALUE);
out.println("R:" + (Integer.MAX_VALUE + 1));
}
}
Build & run:
~/java-quiz$ javac NumberQuiz.java
~/java-quiz$ java NumberQuiz
A:false
B:true
sf:0.6
sd:0.6000000000000001
C:true
D:true
E:true
F:false
G:false
H:false
I:false
J:false
K:false
L:true
M:-1
N:32767
O:32768
P:-32768
Q:2147483647
R:-2147483648
Floating point numbers cannot represent all decimal numbers exactly
(see IEEE 754 below). 0.5
can be represented exactly (because it is
a division of 1
by a power of 2
) and conversion from float
to
double
introduces no error ("Widening Primitive Conversion"; see
[1]). 0.2
cannot be represented this way. When converting float
to
double
an error is introduced.
Funny: when adding up 0.2f
values the result seems more exact than
when adding up 0.2d
(but this is not true for all/any values).
0.0
and -0.0
are considered being equal for double
eventhough
their bit representations are not. Comparing against float
(i.e. conversion to double
) introduces no error -- so they're equal.
Not so for Double
! 0.0
and -0.0
and Float
s are all
non-equal.
For NaN
(not a number) it's different: NaN
is not even equal to
itself (like NULL
in databases is not = NULL
but IS NULL
is used
for testing for NULL
). Then again it's different for Double
.
Integer number types have some strange properties as well: they wrap
around when they reach their MIN_VALUE
/MAX_VALUE
. Note that there
is one more negative value than there are positive values. In the
expression Short.MAX_VALUE + 1
the short
value is converted to
int
and then 1
is added. So there is no wrap around.
[1] https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html
When comparing a float
value against a double
value Java converts
the float
value to a double
value (not the other way around!). You
can check by looking at the byte-code of CompareFloatAndDouble.java
.
import static java.lang.System.*;
class CompareFloatAndDouble {
public static void main(String... args) {
double d = Double.parseDouble(args[0]);
float f = (float) d;
boolean b = d == f;
double fd = f;
out.println("d ="+d+"/"+String.format("%016X", Double.doubleToRawLongBits(d)));
out.println("f ="+f+"/"+String.format("%08X", Float.floatToRawIntBits(f)));
out.println("fd="+fd+"/"+String.format("%016X", Double.doubleToRawLongBits(fd)));
out.println("b="+b);
}
}
Build & run:
~/java-quiz$ javac CompareFloatAndDouble.java
~/java-quiz$ java CompareFloatAndDouble 0.5
d =0.5/3FE0000000000000
f =0.5/3F000000
fd=0.5/3FE0000000000000
b=true
~/java-quiz$ java CompareFloatAndDouble 0.2
d =0.2/3FC999999999999A
f =0.2/3E4CCCCD
fd=0.20000000298023224/3FC99999A0000000
b=false
~/java-quiz$ java CompareFloatAndDouble NaN
d =NaN/7FF8000000000000
f =NaN/7FC00000
fd=NaN/7FF8000000000000
b=false
~/java-quiz$ java CompareFloatAndDouble 0.0
d =0.0/0000000000000000
f =0.0/00000000
fd=0.0/0000000000000000
b=true
~/java-quiz$ java CompareFloatAndDouble -0.0
d =-0.0/8000000000000000
f =-0.0/80000000
fd=-0.0/8000000000000000
b=true
Now look at the byte-code:
~/java-quiz$ javap -v CompareFloatAndDouble
[...]
0: aload_0
1: iconst_0
2: aaload
3: invokestatic #2 // Method java/lang/Double.parseDouble:(Ljava/lang/String;)D
6: dstore_1
Here the double
value gets converted to float
(d2f
):
7: dload_1
8: d2f
9: fstore_3
Here the float
value gets converted (back) to a double
(f2d
) ...
10: dload_1
11: fload_3
12: f2d
... and the two double
values are compared (dcmpl
):
13: dcmpl
14: ifne 21
17: iconst_1
18: goto 22
21: iconst_0
22: istore 4
24: fload_3
25: f2d
26: dstore 5
28: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
[...]
Looking at the bit-representation of the float
and the double
values you can tell that the values for 0.5
look a lot more alike
than the values for 0.2
(details can be found in the "IEEE 754 –
IEEE Standard for Binary Floating-Point Arithmetic for microprocessor
systems (ANSI/IEEE Std 754-1985/IEEE 754-2008)" [1]).
[1] https://en.wikipedia.org/wiki/IEEE_floating_point
import static java.lang.System.*;
import static java.lang.Boolean.*;
class BooleanQuiz {
public static void main(String... args) {
boolean a = new Boolean(true);
boolean b = new Boolean(true);
Boolean A = new Boolean(true);
Boolean B = new Boolean(true);
Boolean C = Boolean.valueOf(true);
Boolean D = Boolean.valueOf(true);
out.println(a == b);
out.println(a == B);
out.println(A == B);
out.println(C == D);
out.println(a == TRUE);
out.println(A == TRUE);
out.println(C == TRUE);
out.println(A.equals(B));
out.println(A.equals(true));
out.println(A.equals(TRUE));
}
}
Build & run:
~/java-quiz$ javac BooleanQuiz.java
~/java-quiz$ java BooleanQuiz
true
true
false
true
true
false
true
true
true
true
Boolean
is not a Java 5 Enum class and thus we can use the public
constructor to create instances. When comparing these via ==
for
identitiy they are different. equals
and auto-(un)-boxing behave
as expected (Boolean.valueOf(boolean)
uses a cache which returns
Boolean.TRUE
and Boolean.FALSE
).
In Clojure you use the boolean [1] literals true
and false
(which
are java.lang.Boolean/TRUE
and java.lang.Boolean/FALSE
at
runtime). Test for equality is done via =
but you usually use
true?
and false?
and the fact that only nil
and false
(or
equally Boolean/FALSE
) are false?
and everything else
(including (Boolean. false)
) is true?
. So when interacting
with Java code you must be carefull when receiving Boolean
values
from the Java-side -- you should convert them via (boolean <java-Boolean>)
.
[1] https://clojuredocs.org/clojure.core/boolean
import static java.lang.System.*;
import java.util.*;
class CollectionSideeffect {
static List sort(List x) {
Collections.sort(x);
return x;
}
public static void main(String... args) {
List a = new ArrayList(Arrays.asList("b", "a", "c"));
List b = sort(a);
List c = a;
c.add(0, "d");
out.println(a);
out.println(b);
out.println(c);
}
}
Build & run:
~/java-quiz$ javac CollectionSideeffect.java
~/java-quiz$ java CollectionSideeffect
[d, a, b, c]
[d, a, b, c]
[d, a, b, c]
This code is similar to the ArraySideeffect
from
above. Collections.sort
is a mutator but we make it look like a
pure function. So a
, b
and c
all reference the same
mutable List
.
In Clojure we have immutable lists and vectors which can be sorted
(like in (sort [6 3 4])
which gives (3 4 6)
).
import static java.lang.System.*;
import java.util.*;
class CorefQuiz {
public static void main(String... args) {
int[][] i = new int[2][];
int[] j = new int[] { 1, 2 };
i[0] = j;
out.println(Arrays.deepToString(i));
j[0]++;
j[1]++;
i[1] = j;
out.println(Arrays.deepToString(i));
}
}
Build & run:
~/java-quiz$ javac CorefQuiz.java
~/java-quiz$ java CorefQuiz
[[1, 2], null]
[[2, 3], [2, 3]]
You cannot only have more than one variable (local on the stack or as a class' field on the heap) reference the same object but you can also have more than one reference within a structure refering to the same object (note that this means you have to expect cyclic structures and even objects with references to themselves!). This also introduces the possibility of side effects.
In this example we have i
-- an array of int[]
. The elements of
the array that i
refers to are references (not int
s!).
After we make i[0]
and i[1]
reference the same object (which is an
int[]
in this case) we can change the int
s in this array and see
the effect of the change in more than one place.
So side effects cannot only be introduced by returning argument reference values (see above) but also by constructing co-refering (any better term for this?) object-graphs to mutable objects.
This can introduce subtle bugs. Imagine a service oriented application where clients (remote and local) call services through their interfaces. When a service is called locally or through a remote RMI client, co-refering object graphs will hit the server/implemenation (since RMI's de-serialization preserves the co-refering graph-structure). But once you switch to REST or SOAP clients the server will see a structure without any co-references (which you could call by value semantics -- see above). This may lead to a different program behaviour.
In Clojure you also have co-references (i.e. sharing) but since things are immutable this causes no problem.
import static java.lang.System.*;
enum EnumQuiz {
FOO, BAR;
public static void main(String... args) {
Object a = FOO;
Object c = valueOf("FOO");
Object d = valueOf(EnumQuiz.class, "FOO");
out.println(a == FOO);
out.println(a.equals(FOO));
out.println(FOO.equals(a));
out.println(a == c);
out.println(a.equals(c));
out.println(c.equals(a));
out.println(a == d);
out.println(a.equals(d));
out.println(d.equals(a));
}
}
Build & run:
~/java-quiz$ javac EnumQuiz.java
~/java-quiz$ java EnumQuiz
true
true
true
true
true
true
true
true
true
For every Enum you have exactly one instance. So you can use the
identity check via ==
when comparing Enums.
Clojure does not support Enums well. You can use the Java Enums but you cannot easely define them. You use namespaced names/vars instead.
import static java.lang.System.*;
class FlowQuiz {
public static void main(String... args) {
try {
for (int i : new int[]{ 2, 1, 3 })
switch (i) {
case 1:
for (int j = 1; j < 5; j++) {
out.println(j);
if (j == 3)
break;
}
break;
case 3:
for (int j = 1; j < 5; j++) {
if (j == 2)
return;
out.println(j);
}
case 2:
for (int j = 1; j < 5; j++) {
if (j == 2)
continue;
out.println(j);
}
default:
out.println("default");
}
out.println("done");
}
finally {
out.println("finally");
}
}
}
Build & run:
~/java-quiz$ javac FlowQuiz.java
~/java-quiz$ java FlowQuiz
1
3
4
default
1
2
3
1
finally
switch/case
with fall-through (i.e. with-out break
) and loops
with break
and continue
are hard to grasp/understand for some
developers. I didn't even put break
and continue
with labels
(structured goto) in there (which many developers don't even know
about).
In Clojure you use recusive looping [1] [2], the sequence abstraction and list comprehension. For example:
-
Produce output 1..4:
(for [i (range 1 5)] (println i))
-
Do not output 2 -- produces 1,3,4.
(doseq [i (remove #{2} (range 1 5))] (println i))
-
Do not output 2 and 3 -- produces 1,4.
(doseq [i (remove #{2 3} (range 1 5))] (println i))
-
Produce until 3 -- produces 1,2.
(doseq [i (take-while #(not (#{3} %)) (range 1 5))] (println i))
or
(doseq [i (range 1 5) :while (not (#{3} i))] (println i))
If you want to really control the loop variable you can use
loop
. This produces 1,2,3:
(loop [i 1]
(when (<= i 3)
(println i)
(recur (inc i))))
In none of these examples you have a mutable variable which could be used outside of the loop by mistake. All you get is successive bindings of a name to different immutable values.
Early returns are imperative trickery. In Clojure the semantics of
execution follow the structure of code much more than in Java. Things
like early returns, break
or continue
are only structured
gotos which you cannot do in Clojure.
[1] https://clojure.org/about/functional_programming#_recursive_looping
[2] https://en.wikibooks.org/wiki/Clojure_Programming/Examples/Cookbook#Looping
import static java.lang.System.*;
class Loops {
public static void main(String... args) {
int[] a = new int[] { 3, 2, 1, 0 };
int j = 0;
do
out.println(j);
while (a[j++] != 1);
for (int i : a)
if (i == 1)
continue;
else
out.println(a[j]);
for (int k = 0;; k++)
if (a[k] > 2)
out.println(k);
else
break;
}
}
Build & run:
~/java-quiz$ javac Loops.java
~/java-quiz$ java Loops
0
1
2
0
0
0
0
Loops are "fun" in Java if you like. while
loops do not scope
looping variables so you have to define them in the surrounding scope
(which may be larger than you want -- but you can use a block scope).
import static java.lang.System.*;
import java.util.*;
class Mutable {
public static void main(String... args) {
Set set = new HashSet();
String[] strings = new String[] { "zoo", "foo", "bar" };
List list = Arrays.asList(strings);
set.add(list);
out.println("set.size() = " + set.size());
out.println("set = " + Arrays.toString(set.toArray()));
out.println("set.contains(list) = " + set.contains(list));
out.println("list = " + list);
Arrays.sort(strings);
out.println("list = " + list);
out.println("set = " + Arrays.toString(set.toArray()));
out.println("set.contains(list) = " + set.contains(list));
}
}
Build & run:
~/java-quiz$ javac Mutable.java
~/java-quiz$ java Mutable
set.size() = 1
set = [[zoo, foo, bar]]
set.contains(list) = true
list = [zoo, foo, bar]
list = [bar, foo, zoo]
set = [[bar, foo, zoo]]
set.contains(list) = false
Mutable keys are bad for you (HashSet
, HashMap
, TreeSet
). The
set does not contains
the element (a List
in this case) which is
its first element -- "confusing".
Which of the four null-checks are "correct"? The code is supposed to
check if the argument is null
(like in a check to prevent a
NullPointerException
when re-referencing x
).
import static java.lang.System.*;
class NullCheck {
static Object NULL = null;
static void foo(Object x) {
if (NULL.equals(x)); // 1
if (x.equals(NULL)); // 2
if (x == NULL); // 3
if (NULL == x); // 4
}
public static void main(String... args) {
// foo(...);
}
}
Option // 1
causes a NullPointerException
since NULL.equals()
de-references null
. Option // 2
will cause a
NullPointerException
in the case "x is null". So both are wrong.
Options // 3
and // 4
both work.
In Clojure you use nil?
without trouble.
import static java.lang.System.*;
interface X { void foo(Object x); }
class A implements X {
public void foo(Object x) { out.println("A1" + x); }
public void foo(String x) { out.println("A2" + x); }
public void foo(Integer x) { out.println("A2" + x); }
public String toString() { return "A"; }
}
class B implements X {
public void foo(Object x) { out.println("B1" + x); }
public void foo(String x) { out.println("B2" + x); }
public void foo(Integer x) { out.println("B3" + x); }
public String toString() { return "B"; }
}
class OverLoad {
public static void main(String... args) {
X a = new A();
B b = new B();
a.foo("foo");
b.foo("bar");
a.foo(1);
b.foo(2);
new A().foo("fred");
new B().foo(a);
}
}
Build & run:
~/java-quiz$ javac OverLoad.java
~/java-quiz$ java OverLoad
A1foo
B2bar
A11
B32
A2fred
B1A
Overloading and overriding are confused by many developers. One of
the frequent error is that developers believe that a.foo("foo")
calls A.foo(String)
which it doesn't. But in new A().foo("fred")
it does -- of course.
In Clojure you use "interfaces" (protocols) and implemenations for these. But usually you do not use inheritence much. For Java-interop you can extends Java classes without trouble.
OK -- this one is silly.
import static java.lang.System.*;
class ScopeQuiz {
static int i = 0;
String x = "foo";
ScopeQuiz(String x) {
this.x = x;
}
ScopeQuiz(int x) {
this("" + x);
out.println(this.x + i);
}
public String toString() { return x + i; }
public static void main(String... args) {
out.println(new ScopeQuiz("bar"));
out.println(i++);
{
int i = 1;
new ScopeQuiz(i++);
}
out.println(i++);
new ScopeQuiz(i++);
}
}
Build & run:
~/java-quiz$ javac ScopeQuiz.java
~/java-quiz$ java ScopeQuiz
bar0
0
11
1
23
import static java.lang.System.*;
class StringQuiz {
public static void main(String... args) {
String a = new String("a");
String b = new String("a");
out.println("a" == "a");
out.println(a == "a");
out.println(a == b);
out.println(a.equals(b));
a.concat("b");
out.println(a);
out.println(a == b);
out.println(a.equals(b));
}
}
Build & run:
~/java-quiz$ javac StringQuiz.java
~/java-quiz$ java StringQuiz
true
false
false
true
a
false
true
String
is immutable and so a.concat("b")
doesn't change anything
but returns a new String
. String
literals are interned
(i.e. pooled) and so "a"
is identical to "a"
in "a" == "a"
.
Some developers misbelieve that String
comparison can always be done
by ==
(because "a" == "a"
) since they do not know about interned
String
s.
In Clojure you use (= "a" x)
which behaves like String.equals
but
handles null
(Clojure nil
) without throwing. Clojure interns
String
literals as well.
(= "foo" "foo") ; -> true
(= nil "foo") ; -> false
(identical? "foo" "foo") ; -> true
(identical? (String. "foo") "foo") ; -> false
(identical? (.intern (String. "foo")) "foo") ; -> true
import static java.lang.System.*;
class TryCatchFinally {
public static void main(String... args) {
out.println(foo(args));
}
public static int foo(String... args) {
int i = 0;
oops: try {
out.println("foo" + args[0]);
if (true)
return i++;
}
catch (Throwable t) {
out.println("catch");
return i++;
}
finally {
out.println("finally" + i);
if (args.length > 0 && "foo".equals(args[0]))
break oops;
}
out.println("done" + i);
return i;
}
}
Build & run:
~/java-quiz$ javac TryCatchFinally.java
~/java-quiz$ java TryCatchFinally
catch
finally1
0
~/java-quiz$ java TryCatchFinally foo
foofoo
finally1
done1
1
~/java-quiz$ java TryCatchFinally bar
foobar
finally1
0
Maybe the foo
case is the most surprising. finally
can introduce
its own termination reason (via return
or break
). So eventhough
the try
block is terminated by return
the finally
block can
overrule this by break
. The semantics (the question "when does it
happen?") of the post-increment (return i++
) are also interesting.
In Clojure we don't have return
and break
but we do have
try/catch/finally
. finally
is evaluated only for side effects
(like closing streams etc.) but not for returning results. So the
result of a try/catch/finally
always comes from the try
expression
or the catch
expression.