Nailing Java Interviews

23 minute read

I have written up this monstrous guide back in 2016 to help a friend land a Java job. Interviews are personality tests to see if the candidate is going to be a good office slave. A good office slave cannot code well, but they eagerly administrate and document everything, work in a way so they can be easily replaced, and can be held responsible for missed deadlines by giving them too much responsibility. Good office slaves are transparent and communicate their actual status with all their actions so management does not have to walk up to them personally.

With all that positivity out of the way, this guide is very likely to be incorrect at places as well as outdated, but it gives a good idea on what the important topics are. These are monitored with high priority during interview sessions.

Version Control Systems

Every codebase should have a version control system. That is Git, SVN, or the less popular Mercurial. These systems are not only for keeping backups of the codebase but to organize and drive a workflow. Branching is an essential strategy to coordinate a team so they are not blocking each other while working on the same codebase.

When the interviewer asks the developer to describe their daily workflow, they want to hear about using version control systems, even if it is their one-man-army hobby project. The good answer is having a protected master branch that is essentially the stable version of the codebase. The second branch should be maintained for development, where different features and fixes land when they are finished. The features and fixes all have their own branches though, usually named after their issue ticket. When they are done, they are requested to be merged to the development branch - by a pull request when using Git - where their integration is to be tested with the other pending changes. A release is usually the promotion of the development branch, in which multiple commits are merged into the master branch. This usually is a restricted operation in teams.

Before merging anything anywhere, code reviews should take place. That is, having someone else objectively check the changes in the code to prevent obvious bugs. Another check is done automatically by static analyzers and different quality metrics tools that reject the pull request if it does not live up to the well-defined quality standard and coding rules of the project. Yes, all projects should have that.

Some of these tools for Java are CheckStyle, FindBugs, PMA and SonarQube.

Coding Standards

Clean Code is a very well-known book written by Robert Martin (Bob Martin, Uncle Bob - big name in the IT industry) which gives a good idea on how code should be written. The main aspects of a clean codebase are maintainability, readability, scalability and testability.

Most principles from Clean Code are as simple and useful as meaningful naming for fields, methods, classes and interfaces. Classes, interfaces and fields should be nouns, methods should be verbs, getters (non-boolean) should start with get and getters for booleans should start with is, has or does. There should be no magic constants in the code, method signatures should have at most 2 parameters, but the least is the best, methods should be short (5 - 10 lines of code) and the same applies to classes (less than 100 lines of code). Clean code should have no comments as the code is supposed to be self explanatory and easy to read.

Java Docs are NOT the same as comments! Java Docs are important especially when developing a public API. At least all public methods should have an attached Java Doc.

Minor acronym principles are DRY (Don’t Repeat Yourself), KISS (Keep It Simple, Stupid) and YAGNI (You Ain’t Gonna Need It). While DRY is obvious, KISS is writing simple and obvious code instead of hacky, ugly, shorter and non-self explanatory code. YAGNI states that only do what must be done - do not add more, needless functionality just by thinking “Hey, this might be useful later on!”.

Testing

Testing is essential and there are no excuses for not writing automated tests - everyone makes bugs. For Java there is JUnit and TestNG, they are both advanced but easy-to-yse test frameworks. The test pyramid contains at least 3 segments:

  1. System Tests
  2. Integration Tests (behavior of components working together)
  3. Unit Tests (independent behavior of components)

Unit Tests are the large base of the pyramid: most of the tests should be Unit Tests.

The middle of the pyramid are Integration Tests. There should be less Integration Tests than Unit Tests, more than System Tests.

When writing Unit Tests for a class which has a single or multiple dependencies, a good practice is to mock them. When mocking a class, its functionality is replaced with a stub which hard-codedly returns the desired values for method calls, removing the necessity of the original class. This process is mocking.

Code coverage is a number for statistical measurements. It represents the percentage of code which are executed during the run of the tests.

Code coverage is not always a good metric itself. The quality of tests, and testing for edge cases are more valuable.

Java

Basics

In terms of usage of the versions, 1.7 > 1.8 > 1.6 as of May 2016.

Java runs on the JVM, a virtual machine (following the Java Language Specification) which interprets bytecode (the compiled .java files, which are the same as .class files). The JVM also has a Just In Time (JIT) compiler which is a complex optimization solution for Java to enhance performance on important points of the code by swapping the bytecode to native code. The most used concrete implementation of the JVM (and JIT) is HotSpot by Sun.

Constants are defined as private static final String MY_CONSTANT = "Hello World.";.

Visibility levels:

public - inherited, visible
protected - inherited, invisible
private - not inherited, invisible
package private (not typed out) - inherited and visible only in the same package

Diamond operator: in early versions of Java it was necessary to type List<String> names = new ArrayList<String>(); which is redundant as String is defined twice. Since Java 1.7 it is optional and List<String> names = new ArrayList<>(); compiles correctly. <> is the diamond operator.

Abstract classes and interfaces are different. A class can implement multiple interfaces, but can only extend a single class. Abstract classes can hold logic, while interfaces only hold signatures.

Annotations in Java are labels above classes, fields and methods providing additional data which does not mainly affect the code itself, just provides information for the compiler.

@Override above a method signature shows that a method is overridden within a subclass. @Deprecated causes warning during compilation, stating that the annotated method is deprecated and should not be used.

Annotations can be created similar to classes, and frameworks such as Spring use them to simplify and automatize development work drastically.

Jar is essentially a zip file for holding metadata and class files. Classpath is the list of directories or jar files to use as dependencies in the application.

Java always passes both primitives and objects by value. In other word, Java is a pass-by-value language instead of pass-by-reference. Passing an int to a method which mutates its value will result in the original int being unchanged. Passing an Object reference to a method which creates a new Object for that reference, will have the original Object also remain unchanged. See this snippet to understand this behavior:

Object object = new Object(1);
mutateObject(object);
// object still has the value 1 at this point, the original reference was not overwritten during the execution of mutateObject.
public void mutateObject(Object object) {
    object = new Object(2); // does not overwrite original object passed in the parameter
}

Autoboxing (and auto-unboxing) allows the interchange of primitives and their corresponding wrapper classes during compilation. For example, an int primitive and an Integer wrapper class will be able to convert from either to the other automatically. The full list of primitives and their corresponding wrappers are here:

boolean  ->  Boolean
char     ->  Char
byte     ->  Byte
short    ->  Short
int      ->  Integer
long     ->  Long
float    ->  Float
double   ->  Double

Demonstration of auto-(un)boxing (all these compile correctly):

Integer a = 10;
int b = a;
int c = 3;
Integer d = b + c;

Immutability means that the state of an object cannot be changed after creation. Achieving immutability for an object results in many advantages such as thread safety, cacheability and consistency of hashCode (good for Maps, Sets). To make an object immutable, all object references should be copied during instantiation. See code below to understand.

public class FaultyImmutableClass {
    
    private List<Object> list;

    public FaultyImmutableClass(List<Object> list) {
        this.list = list;
    }

}

Even though FaultyImmutableClass has no mutators, its internal state can be changed through list. See how:

    List<Object> list = new ArrayList<>();
    FaultyImmutableClass object = new FaultyImmutableClass(list);
    list.add(new Object());

The List of FaultyImmutableClass will be changed, resulting in the class not being immutable. To fix this issue, this is the right snippet:

public class FaultyImmutableClass {
    
    private List<Object> list;

    public FaultyImmutableClass(List<Object> list) {
        this.list = Collections.unmodifiableList(list);
    }

}

From JavaDoc, Collections.unmodifiableList(List<? extends T> list): Returns an unmodifiable view of the specified list.

String is immutable. When concatenating with the + operator, a new String object is created instead of modifying the already existing objects.

Overloading and overriding are coming from the nature of being an object oriented language, see Polymorphism section for details. Method signatures are related to this topic, and can be also found there.

Generics

Generic classes and methods allow the specification of multiple data types which are applicable for some algorithms. To immediately gain insight, remember how List works in Java.

List has a type parameter which is typed inside the angle brackets. By default, List accepts <? extends Object> which is any type or class which is a subclass of Object or is Object. When declaring a List of String objects, the type parameter is set to String by typing List<String> strings;. This guarantees compile type safety as only String objects can be added to the List, and String objects are returned by get() calls. After compilation, generics are erased (this process is the type erasure) for backward compatibility in the bytecode.

This is an example of a generic class with generic methods.

public class GenericClass<T> {

    private T object;
        
    public void set(T object) {
        this.object = object;
     }

    public T get() {
        return object;
    }

}

During the instantiation of GenericClass, its type parameter T can be set to any type. Unless specified, a type parameter extends Object and is not restricted by any means.

GenericClass<Integer> integerHolder = new GenericClass<>();
integerHolder.set(10);
integerHolder.get();

This code compiles correctly. To restrict the type parameter to only allow numbers for example, <T> should be replaced with <T extends Number>.

Object

Object is the superclass of all Java objects, and its methods should be well-known:

protected Object clone() throws CloneNotSupportedException
    Creates and returns a copy of this object.
public boolean equals(Object obj)
    Indicates whether some other object is "equal to" this one.
protected void finalize() throws Throwable
    Called by the garbage collector on an object when garbage collection determines that there are no more references to the object.
public final Class getClass()
    Returns the runtime class of an object.
public int hashCode()
    Returns a hash code value for the object.
public String toString()
    Returns a string representation of the object.

finalize() should NEVER be used.

hashCode() is a numerical representation of the object and its internal state at the time of calling the method. hashCode() is used by HashMap for instance to provide better searching algorithms. equals() is the equality check with another object of the same class. While two equal objects always have the same hashCode, two objects with the same hashCode are not guaranteed to be equal as hash collision is possible.

Memory Model

There are two major parts of the JMM. These are heap memory and stack memory.

Heap memory is used for the allocation of objects and classes. Heap memory space is managed by the Garbage collector which periodically runs, stops threads for that time and frees memory taken by dereferenced objects (~objects which are not referenced anywhere). This is how Java automatically handles its memory. Whenever an object is created, it takes space from heap memory. All objects stored in Heap memory can be accessible from any point of the application.

Due to optimization purposes, heap memory is further divided into multiple parts: young-generation and old-generation. Newly created objects go to young-generation, and after surviving multiple iterations of garbage collections, they get repositioned to old-generation, which is checked by the garbage collector less often. There is also a special part of heap memory, which is the PermGen space (changed to Metaspace as of Java 1.8). PermGen space holds static references and user-defined classes. When heap space runs out, an OutOfMemoryError is thrown.

Stack memory is used for the execution of the current thread. For method calls, stack memory holds all necessary variables and objects. Stack memory operates with the Last In First Out (LIFO) order. Compared to Heap memory, stack memory is much faster but has drastically less capacity. When that capacity runs out, a StackOverFlowError is thrown. Stack memory cannot be accessed by other threads.

Tools

Java has a huge community. Being a very popular language, there are dozens of tools developed which aid the work in many different ways.

Build tools are used to compile and package projects whilst handling dependencies automatically. Maven, Gradle and Ant are the most popular build tools.

Static code analysis generates feedback and analytic on a code snippet or a full project based on rulesets and settings. Static code analysis helps in catching errors before compilation and gives an idea on writing better code. SonarQube provides all these features and even more, it is popular and definitely a good choice for this task.

Continuous integration is the process in which an automatized system continuously delivers the software. Take this example: upon a commit, a pre-hook fires and checks the code for errors and quality - if successful, another script builds the project from the source and runs the corresponding tests - if the tests pass, the code goes into the production environment. Jenkins is a sandbox tool which makes continuous integration and delivery possible.

Design patterns

Design patterns are solutions to regularly reoccurring problems in development. The usage of design patterns enhance the quality of code while maintaining its cleanness. The Gang of Four wrote a book on this topic (Design Patterns) in which they collected design patterns under three categories.

Creational Patterns

Creational patterns are used to separate the creational and behavioral logic of a class.

Factory Pattern is the most simple Creational pattern. Having a Color class with red, green and blue fields, the ColorFactory class comes useful with static methods such as getRed() or getCyan() which create a new Color object and return them accordingly. This use case is shows that Factory is useful for predefined and named object creation. Another use case is handling complex creational logic inside the Factory instead of in the original class.

Builder Pattern is another Creational pattern. Instead of having a complex constructor with many parameters, defining methods which set the fields one-by-one and return the object is a good practice. Chaining these calls result in an easy-to-read setup of the object.

new Car("Volvo", 4, 250, 1) can become more descriptive and easy-to-use as seen:

new Car("Volvo")
    .withSeats(4)
    .withHorsePower(250)
    .withFuel(1)

To achieve this, the class Car should be implemented the following way:

public class Car {
    
    private String brand;
    private int seats;
    private int horsePower;
    private int fuel;   

    public Car(String brand) {
        this.brand = brand;
    }

    public Car withSeats(int seats) {
        this.seats = seats;
        return this;
    }

    public Car withHorsePower(int horsePower) {
        this.horsePower = horsePower;
        return this;
    }

    public Car withFuel(int fuel) {
        this.fuel = fuel;
        return this;
    }

}

Singleton Pattern is for ensuring that only a single object exists of a class. A getInstance() method retrieves that single and same object all the time, or if it does not exist yet, creates it without revealing its logic. The default constructor is disabled by setting its visibility to private.

public class Singleton {

    private static Singleton INSTANCE;

    private Singleton() {}

    public static void getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

}

Singleton is sometimes considered as an anti-pattern. Reading more about it is highly advised as this is a common topic for interviews.

Structural Patterns

Adapter fixes incompatibility between classes by taking the output of a class and converting it accordingly to the input of another class that will use it.

Take the following example. It is required to connect ReportGenerator with MonthlyReportSummarizer, but that takes the Report objects as a Set, making the two incompatible.

public class ReportGenerator {
    
    public Report generateReport() { ... }

}
public class MonthlyReportSummarizer {

    public Summary generateSummaryFromReports(Set<Report> reports) { ... }

}

Here is an implementation of the adapter class which will convert a single or multiple Record objects to a Set which can be used by MonthlyReportSummarizer.

public class ReportSummaryAdapter {

    public Set<Report> createSetFromReports(Report... reports) { ... }

}

Bridge is the same as the Adapter pattern above, however a major difference is that while the Adapter is used afterwards the two interfaces are implemented, Bridges are created before.

Decorator is used to enhance the behavior of an existing class.

Suppose the existence of the class ByteReader which can read an array or stream of bytes one-by-one. An implementation of the functionality to read by lines instead of bytes is needed.

An option would be to use the Decorator pattern and implement LineReader which decorates ByteReader.

public class LineReader {

    private ByteReader byteReader;

    public LineReader() {
        byteReader = new ByteReader();
    }

    public String readLine() {
        String line = "";
        byte currentByte = byteReader.read();
        if (currentByte == NEW_LINE) {
            return line;
        }
        line += currentByte;        
    }

}

Facade is a class which contains shortcuts for method calls in a complex system.

Proxy is another structural design pattern. By creating a Proxy for a class, it should have the same public interface which provides the same results for method calls, but further functionality can be added. Such functionality can be logging for example. The end user should not see any difference in the behavior when using the proxied object. A proxy should have a composition relationship with the proxied object and delegate all calls to it to provide the same behavior.

public class ListProxy {

    private List<Object> list;

    public ListProxy() {
        list = new ArrayList<>();
    }

    public void add(Object object) {
        Logger.log("Added object: " + object.toString());
        list.add();
    }

    public Object get(int index) {
        Logger.log("Got object: " + object.toString());
        return list.get(index);
    }

}

Behavioral Patterns

Command is implemented as a class which stores a command, such as a step in a game of chess. This allows the step/command to be stored, reverted, etc. since it has a representation in the form of an object.

This interface below is less by far from what the Command pattern can do, but is a simple and worthy example.

interface Command {
    void apply();
    void revert();
} 

Iterator enables looping through an array or a collection and doing basic operations on the current element.

interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Strategy is used to change the logic of an algorithm during runtime with ease. Suppose the following example.

The Person class has an inner logic to eat. The algorithm for eating is unspecified in Person, EatStrategy will hold it instead. Since eatStrategy is an object which can be changed anytime, the algorithm for eating can change anytime as well.

public class Person { 
    
    private int hoursSinceLastMeal;
    private EatStrategy eatStrategy;

    public void logic() {
        if (hoursSinceLastMeal > 8) {
            eatStrategy.eat();
        }
    }

    public void setEatStrategy(EatStrategy eatStrategy) {
        this.eatStrategy = eatStrategy;
    }

}
interface EatStrategy {
    void eat();
}

public class ClosestEatStrategy implements EatStrategy {
    public void eat() {
        // eat the closest food
    }
}

public class CheapestEatStrategy implements EatStrategy {
    public void eat() {
        // eat the cheapest food
    }
}

Template pattern works similarly to Strategy pattern. Instead of switching implementations of a common interface, Template defines an abstract superclass holding the skeleton and core logic which (or its substeps) will be later specified or changed in the subclasses. The most common example for this pattern is the following:

public abstract class Human {

    public void doDailyRoutine() {
        getUp();
        doWork();
        goToSleep();
    }

    abstract void getUp();
    abstract void doWork();
    abstract void goToSleep();
}

The abstract superclass Human can be extended via inheritance to define multiple different logic for this schema, skeleton or template.

public class Police extends Human {

    public void getUp() {
        // take gun
    }

    public void doWork() {
        // arrest criminals
    }

    public void goToSleep() {
        // put gun under bed
    }

}

public class Developer extends Human {
    
    public void getUp() {
        // start thinking in binary
    }

    public void doWork() {
        // code
    }

    public void goToSleep() {
        // stop thinking in binary
    }
}

Object Oriented Principles

Here is a short code snippet which will be used as an example below.

public class Human {
    private double energy = 100.0;

    public void doDailyRoutine() {
        // <...> work
        removeEnergy(10.0);
    }

    public void removeEnergy(double amount) {
        energy -= amount;
    }
}

public class Soldier extends Human {
    @Override
    public void doDailyRoutine() {
        // <...> fight
        removeEnergy(90.0);
    }
}

Encapsulation

Grouping the logically similar data types and structures together, such as creating a class for cohesive methods and fields. This also allows information hiding. By creating classes, the developer has control over their fields and methods. Given that a field should be modifiable, a corresponding mutator / setter eg. removeEnergy() should be created, providing the only way to modify energy. Since there is no accessor / getter, the energy field of the Human class remains hidden. This is information hiding, which is a tool of encapsulation, not encapsulation itself.

Abstraction

Abstraction is the way do define data types or structures similar to a real world scenarios, hiding the implementation and providing interfaces to access or mutate it. In Java, abstract data types can be instantiated (creating a concrete instance of a class) and have the defined operations performed on it. Complex data structures are often represented with multiple smaller data structes. An Employee has a Human data structure as well as a Worker for example.

Inheritance

Classes have the ability to extend existing classes using the extends keyword. All the non-private fields and methods are present in the new class, which is the subclass. This process is inheritance. This provides the ability to override (@Override) functions to change their behavior (see Runtime Polymorphism), and to add more functionality by introducing new fields and methods. Soldier is the subclass of Human, Human is the superclass of Soldier. All protected and public methods and fields are inherited by Soldier (such as removeEnergy()) and can be used right away.

Polymorphism

Polymorphism is a concept in Object Oriented Programming which allows an object or a method to have multiple forms.

An example is referencing an instance of Soldier by its superclass.

Human john = new Soldier();

Object josh = new Soldier(); // Object is the superclass of all classes in Java

There are two versions of polymorphism in Java.

The first version of polymorphism is compile time polymorphism which is the same as method overloading. Method overloading allows multiple methods with the same name but a different signature, which is achievable by a different count of parameters, different type of parameters or both.

“In the Java programming language, a method signature is the method name and the number and type of its parameters. Return types and thrown exceptions are not considered to be a part of the method signature.”

The second version of polymorphism is runtime polymorphism which is the same as method overriding. Methods can be overridden within subclasses (see Inheritance), and the method corresponding to the actual runtime class will be called.

SOLID

S

Single responsibility principle: a class should do only a single task, fulfill a single role.

Classes should be small and only have a single functionality. It is perfectly fine to have a large amount of small classes. Sometimes even the logic of the creation of the object, and the behavior of the object are separated: see Factory and Builder design patterns for example. Another way to put this principle is this: an object should only have a single reason to change.

O

Open/closed principle: a class should be open for extension, but closed for modification.

To further develop a class, it should not be changed as that can break its logic; it should be extended instead. Extension can be done via inheritance, or by composition. See Decorator design pattern for further description.

L

Liskov substitution principle: a subclass should be able to fulfill the role of its superclass.

If class A is the superclass of class B, when class B is cast to be class A, it should behave exactly the same as class A. Subclasses should not change behavior severly to obey this rule.

I

Interface segregation principle: multiple smaller interfaces are better than a single with a larger set of functionalities.

Interfaces can over-flood with unnecessary functionality which has to be implemented whenever using that interface. Therefor it is much better to have more interfaces with smaller functionalities.

D

Dependency inversion principle: a class should depend on abstractions and not on concretions.

High-level modules should not depend on low-level modules, a single or multiple layer of abstraction is good to have.

The Sorting Problem

In Java, the sorting algorithm for primitives is a slightly modified quicksort algorithm. Quicksort is not applicable for objects though. Instead, objects use the Timsort algorithm. To understand why this was necessary, think of the following set of int primitivies.

[4, 6, 5, 2, 5, 5, 9]

The sorted set will look like this.

[2, 4, 5, 5, 5, 6, 9]

As seen above, 5 is a duplicate element. The order of the fives next to each other do not matter. The inner order of the fives may change during quicksorting which is not an issue with primitives - since it is impossible to make difference between multiple fives.

Objects, when sorted, need to implement the Comparable<T> interface or a Comparator<T>. This is the prerequisite for comparing them to each other to decide their order. Consider another set of Gladiator objects to understand the problem.

{ gladiators : [
    { name: Chris, age: 38, weapon: Trident },
    { name: Peter, age: 41, weapon: Katana },
    { name: Stewie, age: 23, weapon: Crossbow },
    { name: Glenn, age: 38, weapon: Club }
] }

The sorting is to be done by age.

{ gladiators : [
    { name: Stewie, age: 23, weapon: Crossbow }, 
    { name: Chris, age: 38, weapon: Trident },
    { name: Glenn, age: 38, weapon: Club },
    { name: Peter, age: 41, weapon: Katana }
] }

Both Chris and Glenn have the same age. The comparator does not specify what to do in situations of equality. The JVM specification does require that the sorted order between equal objects have to maintain their original, unsorted order. In the current scenario meaning, that Glenn has to come after Chris since that was the original order.

To satisfy this requirement, Timsort was chosen as the sorting algorithm for objects, as unlike quicksort, Timsort guarantees the original order of the equal objects.

Further Read

Effective Java (2nd Edition) written by Joshua Bloch

Clean Code written by Robert Cecil Martin

List of interview questions on the web with descriptive answers.

Categories:

Updated: