Project Nayuki


Good Java idioms

There are aspects of programming in Java that are not obvious just by learning from the language specification or standard API documentation. This document attempts to collect and explain the most frequently used idioms, including ones that are difficult to guess correctly (such as implementing a correct equals() method). (This article only scratches the surface; the book Effective Java by Joshua Bloch gives a much more thorough treatment of this topic.)

I hereby place all code on this page in the public domain. Feel free to copy and modify any snippet of code however you like without credit.

Contents


Implementing equals()

class Person {
  String name;
  int birthYear;
  byte[] raw;
  
  public boolean equals(Object obj) {
    if (!obj instanceof Person)
      return false;
    
    Person other = (Person)obj;
    return name.equals(other.name)
        && birthYear == other.birthYear
        && Arrays.equals(raw, other.raw);
  }
  
  public int hashCode() { ... }
}
  • The parameter must be of type Object, not the type of the enclosing class (Person).

  • If foo is not null, then foo.equals(null) must return false, not throw a NullPointerException. (Note that null instanceof AnyClass is always false, so the code above works.)

  • Compare primitive fields (e.g. int) using ==, compare object fields using equals(), and compare array-of-primitive fields using Arrays.equals().

  • When overriding equals(), remember to override hashCode() in a way that is consistent with equals().

  • See: java.lang.Object.equals(Object)

Implementing hashCode()

class Person {
  String a;
  Object b;
  byte c;
  int[] d;
  
  public int hashCode() {
    return a.hashCode() + b.hashCode() + c + Arrays.hashCode(d);
  }
  
  public boolean equals(Object o) { ... }
}
  • When two objects x and y have x.equals(y) == true, you must ensure that x.hashCode() == y.hashCode().

  • By contrapositive, if x.hashCode() != y.hashCode(), then it must be the case that x.equals(y) == false.

  • It is not required that when x.equals(y) == false, you have x.hashCode() != y.hashCode(). But if you can make this occur as often as possible, then it improves the performance of data structures based on hash tables (such as HashSet).

  • The simplest legal implementation of hashCode() is simply return 0;. Although data structures like HashMap will operate correctly, the run time will generally slow down from O(1) for a good hash function to O(n) for a bad degenerate hash function.

  • Java SE 7 introduced a convenience method to compute a hash over many fields: Objects.hash(a, b, c, d)

  • See: java.lang.Object.hashCode(), java.util.Objects.hash(Object...)

Implementing compareTo()

class Person implements Comparable<Person> {
  String firstName;
  String lastName;
  int birthdate;
  
  // Compare by firstName, break ties by lastName,
  // finally break ties by birthdate
  public int compareTo(Person other) {
    if (firstName.compareTo(other.firstName) != 0)
      return firstName.compareTo(other.firstName);
    else if (lastName.compareTo(other.lastName) != 0)
      return lastName.compareTo(other.lastName);
    else if (birthdate < other.birthdate)
      return -1;
    else if (birthdate > other.birthdate)
      return 1;
    else
      return 0;
  }
}
  • Always implement the generic version Comparable<T> rather than the raw type Comparable because it reduces code and hassle.

  • Only the sign of the returned result matters (negative/zero/positive), not the magnitude. So never try to check that compareTo() returned exactly -1 or such.

  • Implementing Comparator.compare() essentially the same as implementing compareTo(). The explicit left-side variable in the former case plays the same role as the implicit this variable in the latter case.

  • Subtracting two integers to produce a comparison is correct if the answer doesn’t overflow, e.g. return birthdate - other.birthdate;. This kind of logic requires extreme care to implement.

  • However, Java SE 7 introduced methods like Integer.compare(x, y) and Long.compare(x, y) to facilitate writing correct comparators without manually writing inequalities.

  • See: java.lang.Comparable, java.lang.Integer.compare(int, int)

Implementing clone()

class Values implements Cloneable {
  String abc;
  double foo;
  int[] bars;
  Date hired;
  
  public Values clone() {
    try {
      Values result = (Values)super.clone();
      result.bars = result.bars.clone();
      result.hired = result.hired.clone();
      return result;
    } catch (CloneNotSupportedException e) {  // Impossible
      throw new AssertionError(e);
    }
  }
}
  • Use super.clone() to offload to the root Object class the responsibility of creating the new instance.

  • The primitive fields are already copied properly. Also, there is no need to clone fields of immutable types such as String and BigInteger.

  • Manually make a deep copy of all the non-primitive fields (objects and arrays).

  • When the class implements Cloneable, clone() will never throw CloneNotSupportedException. So catch the exception and ignore it, or wrap it in an unchecked exception.

  • It’s also legal to implement clone() without using Object.clone(), by manually constructing a new object and copying fields.

  • See: java.lang.Object.clone(), java.lang.Cloneable

Using StringBuilder/StringBuffer

// join(["a", "b", "c"]) -> "a and b and c"
String join(List<String> strs) {
  StringBuilder sb = new StringBuilder();
  boolean first = true;
  for (String s : strs) {
    if (first) first = false;
    else sb.append(" and ");
    sb.append(s);
  }
  return sb.toString();
}
  • Don’t use repeated string concatenation like this because it takes Θ(n2) time: s += item;

  • In StringBuilder or StringBuffer, use append() to add text and toString() to get the entire accumulated text.

  • StringBuilder is preferred because it’s faster. StringBuffer has all synchronized methods, which you usually don’t need.

  • The example code above can be replaced by an API method introduced in Java SE 8, String.join(delimiter, elements...).

  • See: java.lang.StringBuilder, java.lang.StringBuffer

Generating a random integer in a range

Random rand = new Random();

// Between 1 and 6 inclusive
int diceRoll() {
  return rand.nextInt(6) + 1;
}
  • Always use the Java API method to generate random numbers in an integer range.

  • Never try to improvise something like Math.abs(rand.nextInt()) % n because it is biased. Furthermore despite the abs(), the value can be negative when rand.nextInt() == Integer.MIN_VALUE.

  • See: java.util.Random.nextInt(int)

Using Iterator.remove()

void filter(List<String> list) {
  for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
    String item = iter.next();
    if (...)
      iter.remove();
  }
}
  • remove() acts on the most recent item returned by next(). remove() can be called at most once per item.

  • See: java.util.Iterator.remove()

Reversing a String

String reverse(String s) {
  return new StringBuilder(s).reverse().toString();
}

Starting a thread

The following 4 examples all accomplish the same thing, but in different ways.

By implementing Runnable:

void startAThread0() {
  new Thread(new MyRunnable()).start();
}

class MyRunnable implements Runnable {
  public void run() {
    ...
  }
}

By extending Thread:

void startAThread1() {
  new MyThread().start();
}

class MyThread extends Thread {
  public void run() {
    ...
  }
}

By anonymously extending Thread:

void startAThread2() {
  new Thread() {
    public void run() {
      ...
    }
  }.start();
}

By calling a lambda function (Java SE 8+) (this is internally equivalent to the example with Runnable):

void startAThread3() {
  new Thread(() -> {
    ...
  }).start();
}

Using try-finally

Example with I/O stream:

void writeStuff() throws IOException {
  OutputStream out = new FileOutputStream(...);
  try {
    out.write(...);
  } finally {
    out.close();
  }
}

Example with lock:

void doWithLock(Lock lock) {
  lock.acquire();
  try {
    ...
  } finally {
    lock.release();
  }
}
  • If the statement before the try fails and throws an exception, then the finally block won’t execute, but there is nothing to release anyway.

  • If a statement inside the try block throws an exception, then execution will jump to the finally block, execute as much as possible, then jump out of the method (unless there is another enclosing finally block).

  • Java SE 7 introduced the try-with-resources statement (a.k.a. automatic resource management). The statement incorporates a variable declaration into the try-block, and implicitly calls close() finally (requires the object to implement AutoCloseable). The I/O stream example code above can be shortened to just try (OutputStream out = new FileOutputStream(...)) { ... }, with no finally block.


Reading byte-wise from an InputStream

InputStream in = (...);
try {
  while (true) {
    int b = in.read();
    if (b == -1)
      break;
    (... process b ...)
  }
} finally {
  in.close();
}
  • read() either returns the next unsigned byte value (range 0 to 255 inclusive) from the stream or returns −1 if the stream has ended.

  • See: java.io.InputStream.read()

Reading block-wise from an InputStream

InputStream in = (...);
try {
  byte[] buf = new byte[100];
  while (true) {
    int n = in.read(buf);
    if (n == -1)
      break;
    (... process buf with offset=0 and length=n ...)
  }
} finally {
  in.close();
}

Reading text from a file

BufferedReader in = new BufferedReader(
    new InputStreamReader(new FileInputStream(...), "UTF-8"));
try {
  while (true) {
    String line = in.readLine();
    if (line == null)
      break;
    (... process line ...)
  }
} finally {
  in.close();
}
  • The creation of the BufferedReader object is cumbersome. But it’s because Java treats bytes and characters as separate concepts (unlike C, for example).

  • You can replace the FileInputStream with any kind of InputStream, such as Socket.getInputStream().

  • BufferedReader.readLine() returns null when the end of the stream is reached.

  • To read one character at a time instead, use Reader.read().

  • You could use character encodings other than UTF-8, but it is inadvisable.

  • See: java.io.BufferedReader, java.io.InputStreamReader

Writing text to a file

PrintWriter out = new PrintWriter(
    new OutputStreamWriter(new FileOutputStream(...), "UTF-8"));
try {
  out.print("Hello ");
  out.print(42);
  out.println(" world!");
} finally {
  out.close();
}
  • The creation of the PrintWriter object is cumbersome. But it’s because Java treats bytes and characters as separate concepts (unlike C, for example).

  • Just like with System.out, you can call print() and println() on many types of values.

  • You could use character encodings other than UTF-8, but it is inadvisable.

  • See: java.io.PrintWriter, java.io.OutputStreamWriter

Reading/writing an entire file

// Read all bytes from a file
byte[] bytes = Files.readAllBytes(Paths.get("infile.bin"));
// Write all bytes to a file
Files.write(Paths.get("outfile.bin"), bytes);

Charset cs = StandardCharsets.UTF_8;
// Read all lines from a file
List<String> lines = Files.readAllLines(Paths.get("infile.txt"), cs);
// Write all lines to a file
Files.write(Paths.get("outfile.txt"), lines, cs);
  • With Java SE 7+, you can read or write an entire text or binary file in one line of code. This is useful for small files where memory usage is negligible.

  • You can still always handle files the old way, where you open a file stream, open a text stream wrapper if necessary, read/write the data, and close the stream.

  • Before Java SE 7, these 4 convenient file I/O methods were available in third-party libraries – Apache Commons IO’s FileUtils and Google Guava’s Files. The main difference is that both of these APIs use a File argument (more convenient) whereas Java SE’s API uses a Path argument (seems less convenient).

  • See: java.nio.file.Files, java.nio.file.Paths, java.nio.file.Path, java.nio.charset.StandardCharsets

Defensive checking: numerical values

int factorial(int n) {
  if (n < 0)
    throw new IllegalArgumentException("Undefined");
  else if (n >= 13)
    throw new ArithmeticException("Result overflow");
  else if (n == 0)
    return 1;
  else
    return n * factorial(n - 1);
}
  • Never assume that numeric inputs are going to be positive, sufficiently small, etc. Check for these conditions explicitly.

  • A well-designed function should behave correctly for all possible input values. Carefully ensure that all cases are considered and that bad output (such as overflow) is never generated.

Defensive checking: objects

int findIndex(List<String> list, String target) {
  if (list == null || target == null)
    throw new NullPointerException();
  ...
}
  • Never assume that object arguments are not null. Check for this condition explicitly.

  • See: Objects.requireNonNull(T) (Java SE 7+)

Defensive checking: array indexes

void frob(byte[] b, int index) {
  if (b == null)
    throw new NullPointerException();
  if (index < 0 || index >= b.length)
    throw new IndexOutOfBoundsException();
  ...
}
  • Never assume that a given array index is within bounds. Check explicitly.

Defensive checking: array ranges

void frob(byte[] b, int off, int len) {
  if (b == null)
    throw new NullPointerException();
  if (off < 0 || off > b.length
    || len < 0 || b.length - off < len)
    throw new IndexOutOfBoundsException();
  ...
}
  • Never assume that a given array range (i.e. “starting at off, going for len elements”) is within bounds. Check explicitly.


Filling array elements

Using a loop:

// Fill each element of array 'a' with 123
byte[] a = (...);
for (int i = 0; i < a.length; i++)
  a[i] = 123;

Using the standard library method (preferred):

Arrays.fill(a, (byte)123);

Copying a range of array elements

Using a loop:

// Copy 8 elements from array 'a' starting at offset 3
// to array 'b' starting at offset 6,
// assuming 'a' and 'b' are distinct arrays
byte[] a = (...);
byte[] b = (...);
for (int i = 0; i < 8; i++)
  b[6 + i] = a[3 + i];

Using the standard library method (preferred):

System.arraycopy(a, 3, b, 6, 8);

Resizing an array

Using a loop (upsizing):

// Make array 'a' larger to newLen
byte[] a = (...);
byte[] b = new byte[newLen];
for (int i = 0; i < a.length; i++)  // Goes up to length of A
  b[i] = a[i];
a = b;

Using a loop (downsizing):

// Make array 'a' smaller to newLen
byte[] a = (...);
byte[] b = new byte[newLen];
for (int i = 0; i < b.length; i++)  // Goes up to length of B
  b[i] = a[i];
a = b;

Using the standard library method (preferred):

a = Arrays.copyOf(a, newLen);

Packing 4 bytes into an int

int packBigEndian(byte[] b) {
  return (b[0] & 0xFF) << 24
       | (b[1] & 0xFF) << 16
       | (b[2] & 0xFF) <<  8
       | (b[3] & 0xFF) <<  0;
}

int packLittleEndian(byte[] b) {
  return (b[0] & 0xFF) <<  0
       | (b[1] & 0xFF) <<  8
       | (b[2] & 0xFF) << 16
       | (b[3] & 0xFF) << 24;
}

Unpacking an int into 4 bytes

byte[] unpackBigEndian(int x) {
  return new byte[] {
    (byte)(x >>> 24),
    (byte)(x >>> 16),
    (byte)(x >>>  8),
    (byte)(x >>>  0)
  };
}

byte[] unpackLittleEndian(int x) {
  return new byte[] {
    (byte)(x >>>  0),
    (byte)(x >>>  8),
    (byte)(x >>> 16),
    (byte)(x >>> 24)
  };
}
  • Always use the unsigned right shift operator (>>>) for bit packing, never the arithmetic right shift operator (>>).