Coding interviews are aimed at gauging how well-prepared a candidate is in terms of language proficiency, foundational knowledge, problem-solving skills, and soft skills. For a position that explicitly mentions a language, such as a job listing for a Java developer, it makes even more sense to spend some time and energy polishing Java skills by exploring the common interview questions asked in such interviews. Doing this not only allows us to benefit directly from the experience of others but more importantly, it’s an opportunity to learn things that we are not aware are gaps in our knowledge.
In this blog, we cover 20 Java language-specific interview questions on a variety of topics. We’re also including answers to these questions to help you prepare for them. For more interview questions, check out the links at the end of this blog.
Let’s go!
Varargs stands for variable arguments. In order to pass a variable number of arguments of the same type to a method, a varargs variable can be used. The syntax consists of including three ellipses after a data type followed by the variable name.
boolean... variableName
A variable name declared in this manner as a method parameter can then be used inside the method as an array that contains arguments of the declared data type. The method findMax
on line 4 in the widget below inputs an arbitrary number of integers and finds the maximum.
class Compute {// Finds the max of the input parameterspublic float findMax(int... scores) {int max = -1;// Finds the max of all input parameters received in the scores variablefor (int i : scores) {if (max < i) max = i;}return max;}}class Main {public static void main(String[] args) {Compute compute = new Compute();System.out.println(compute.findMax(1, 2, 3, 4, 5));System.out.println(compute.findMax(10, 20, 30));System.out.println(compute.findMax());}}
The @
operator is used for specifying Java annotations that essentially represent metadata associated with Java code, such as a Java method, class, or interface. Some annotations (introduced in Java 7 and Java 8) also apply to other annotations and are called meta annotations.
Java annotations are optional but useful for better code readability. The annotations are also readable by the compiler, and in some cases, errors and warnings are given by the compiler where there is a mismatch between the code and the intent specified by the annotation. For example, consider the following code:
abstract class UniversalGreeting {@Deprecatedpublic abstract void hello();}class French extends UniversalGreeting {public void hello() {System.out.println("Bonjour");}//@Overridepublic void goodbye() {System.out.println("Au revoir");}}class Main {public static void main(String[] args) {French fr = new French();fr.hello();}}
@Deprecated
conveys that the method hello()
is deprecated.@Override
on line 13 that’s commented out specifies that the method following it has to override an existing method. If the method being overridden is missing from the parent class, the compiler will give an error. Uncomment line 13 to see this.It is also possible to define custom Java annotations for custom behavior required at compile-time or runtime.
A Java string can be created using the String
class or directly as string literals (like “Hello”). In Java, strings are immutable objects (unchangeable). String literals are created in an area of heap memory called a string pool. This ensures that any two literals with the same content refer to the same object. This must be kept in mind when programming with Java strings.
In the following code, using the ==
operator works when comparing literals (see line 6), but does not work when an explicit String
object is compared with a string literal (see line 10). The method str.equals("Hello")
can be used instead as shown on line 13 to check for semantic equivalence.
import java.lang.String;class StringExample {public static void main(String[] args) {if ("Hello!" == "Hello!") {System.out.println("Same strings.");}String str = new String("Hello!");if (str == "Hello!") {System.out.println("Different strings.");}if (str.equals("Hello!")) {System.out.println("Semantically, the strings are equal.");}}}
All classes inherit, directly or indirectly, from the Object
class. The equals()
method is implemented in the Object
class to be identical to the ==
operator. A consequence of this is that for reference variables, comparisons made with the default equals()
method will only hold true when the two reference variables contain the same address. Depending on our application, we might want two objects to be equal as long as they are equivalent in some sense. For example, we might want to override the definition of equals()
in the following code if we’d like two ordered pairs with the same x
and y
coordinates to represent the same point in the plane.
class Pair {int x;int y;// ConstructorPair(int x, int y) {this.x = x;this.y = y;}}class Main {public static void main(String[] args) {Pair p1 = new Pair(1, 2);Pair p2 = new Pair(1, 2);System.out.println("Does p1 equal p2? " + p1.equals(p2));}}
Note that according to The Java Language Specification, any implementation of equals()
must test for equivalence under an equivalence relation. In particular, for objects that are not null
, an implementation should ensure the following properties:
a
equals b
, then b
equals a
(symmetric).a
equals b
and b
equals c
, then a
equals c
(transitive).The method hashcode()
returns an integer that’s used by Java collections, such as HashMap
, HashSet
, etc., to store an object against its numeric hash code. According to The Java Language Specification for hashcode()
, whenever two objects are equal, they must have the same hash code.
In the following code, we override equals()
for the class State
. So, the two State
objects on line 38 are equal, but because we made the mistake of not overriding hashcode()
, we see that such an instance cannot be used for retrieving the required data from the hash map (see lines 40–43).
import java.util.HashMap;class State {String abbreviation = "";String name = "";State(String a, String n) {this.abbreviation = a;this.name = n;}@Overridepublic boolean equals(Object obj) {State st = (State) obj;if (this.abbreviation.equals(st.abbreviation) && this.name.equals(st.name)) {return true;} else {return false;}}}class Main {public static void main(String[] args) {// Map to store data of a state against each stateHashMap<State, String> stateData = new HashMap<State, String>();stateData.put(new State("NY", "New York"),"Voter registration deadlines for NY");System.out.println("Are objects equal? " +(new State("NY", "New York").equals(new State("NY", "New York"))));String retrieved = stateData.get(new State("NY", "New York"));System.out.println("What does the object retrieve from the hashmap? " + retrieved);}}
A generic type is a class or an interface that’s defined using one or more parameters that serve as placeholders for other types. For instance, in the following code, the appearance of T
in angular brackets on line 3 indicates that the class MyClass
is parametrized over the type T
. This makes MyClass
a generic class.
We create a generic type GenericQueue<T>
on lines 3–16 and instantiate it by substituting the parameter T
by a type argument as shown on line 21 and line 26 in the widget below.
import java.util.LinkedList;class GenericQueue<T> {LinkedList<T> queue = new LinkedList<T>(); // A list containing objects of type T// Inserting an object of type T at the endvoid enqueue(T t) {queue.addLast(t);}// Removing an object of type T from the queueT dequeue() {return queue.removeFirst();}}class Main {public static void main(String[] args) {GenericQueue<String> q1 = new GenericQueue<String>();q1.enqueue("Asia");q1.enqueue("Africa");System.out.println(q1.dequeue());GenericQueue<Integer> q2 = new GenericQueue<Integer>();q2.enqueue(1);q2.enqueue(2);System.out.println(q2.dequeue());}}
Note: The type parameters, such as
T
in the code above, bind to non-primitive data types only.
The biggest advantage of using a generic type is that the same code can be reused multiple times for different types.
A Java type such as List<Integer>
is neither a subtype of List<Object>
nor its supertype, even though Integer
is a subtype of Object
.
Suppose Child
is a subclass of the Parent
class. In type theory:
T<Child>
is a subtype of T<Parent>
, it is said to be covariant.T<Parent>
is a subtype of T<Child>
, it is said to be contravariant.Java generic types are invariant. So, in the following code, passing an instance of type Generic<Integer>
to a method that expects an instance of type Generic<Object>
gives a compiler error on line 13 and vice versa on line 12.
class Generic<T> {public T t;}class Main {public static void main(String[] args) {Generic<Object> o = new Generic<Object>();Generic<Integer> i = new Generic<Integer>();paramBoundToInteger(o);paramBoundToObject(i);}public static void paramBoundToObject(Generic<Object> o) {System.out.println("An object of type Generic<Object> is passed an argument.");}public static void paramBoundToInteger(Generic<Integer> i) {System.out.println("An object of type Generic<Integer> is passed an argument.");}}
A thread can be created by either extending the Thread
class or by implementing the Runnable
interface. The latter option is especially useful if we’re already inheriting from a class other than Thread
.
In the following code, we can see both approaches:
MyThread
extends from Thread
and an instance of MyThread
is created to run the thread.YourThread
implements the Runnable
interface. Its instance is passed to a Thread
constructor to create a thread.// One way to create a threadclass MyThread extends Thread {public void run() {System.out.println("I am a thread created via extension of the Thread class");}}// Another wayclass YourThread implements Runnable {public void run() {System.out.println("I am a thread implemented via Runnable");}}class Main {public static void main(String[] args) {// MyThread is a subclass of ThreadMyThread mt = new MyThread();mt.start();// YourThread implements RunnableThread yt = new Thread(new YourThread());yt.start();}}
In older Java versions, it used to be the case that the interfaces strictly contained method signatures for which implementations were not provided. Such methods are called abstract methods. A class that implements an interface needs to provide the definitions of all abstract functions in the interface.
Starting with Java 8, we can implement certain methods within an interface. These methods are specified with the default
and static
keywords and are known as default methods and static methods, accordingly. For instance, in the following code, the interface defined on lines 1–14 defines an abstract, a default, and a static method:
interface MyInterface {// An abstract method with no implementation givenpublic void acknowledge();// A default methodpublic default void greet() {System.out.println("Hello!");}// A static methodpublic static void farewell() {System.out.println("Good bye!");}}class MyClass implements MyInterface {// Implementation given for the abstract method in the interface@Overridepublic void acknowledge() {System.out.println("Nod :)");}}class Main {public static void main(String[] args) {MyClass mc = new MyClass();mc.greet();mc.acknowledge();MyInterface.farewell(); // Static method is invoked on the class and not an instance}}
Note that a static method that belongs to an interface, much like a static method that belongs to a class, is not inherited.
An interface that contains exactly one abstract function is a functional interface. A functional interface can include multiple default and static methods. It’s a good programming practice to use the annotation @FunctionalInterface
to mark an interface as a functional interface.
Some well-known examples of functional interfaces are Comparable
with the abstract method compareTo
,
and Runnable
with the abstract method run
.
In Java 8, java.util.function
contains, among others, the following four useful functional interfaces that we should be familiar with.
Interface | Description of abstract method |
| Takes an input and returns a value. |
| Takes an input and returns |
| Takes an input and returns nothing. |
| Takes no input and returns a value. |
Lambda expressions, introduced in Java 8, are objects that implement a functional interface. The syntax of a lambda specifies the definition of the sole abstract method of a functional interface.
This syntax consists of three parts:
->
.->
is followed by a block of code included within braces {}
. The braces can be omitted in case the block consists of just a single expression corresponding to the value being returned.For example, the following lambda takes two numeric arguments and returns the sum of these.
(int a, float b) -> {return a+b;};
Note: The types of variables in the lambda can be specified explicitly but can also be inferred from the surrounding context.
The following code shows two different ways to write the same lambda expression. The lambda expressions on line 5 and lines 7–10 take no argument. They simply print a message and return nothing. Note that the right-hand sides give an implementation of the run
method of the Runnable
interface. The objects created on line 5 and lines 7–10 therefore implement the Runnable
interface and can be used to create threads on line 12 and line 14.
class Main {public static void main(String[] args) {//The body is an expressionRunnable r1 = () -> System.out.println("I am r1");//The body consists of two statementsRunnable r2 = () -> {System.out.println("I am r2");return;};Thread t1 = new Thread(r1);t1.start();Thread t2 = new Thread(r2);t2.start();}}
The forEach
loop can be used to iterate over a collection. Its argument must be an instance that’s an implementation of the Consumer
functional interface. A lambda argument passed as an argument to the loop is applied to each element of the collection. For instance, in the following code on line 11, the lambda expression is applied to each element, i
, in a list. The input i
is consumed, and nothing is returned.
import java.util.ArrayList;class Main {public static void main(String[] args) {ArrayList<String> list = new ArrayList<String>();list.add("Alpha");list.add("Beta");list.add("Gamma");// For each element i, true is printed if it contains the capital letter Alist.forEach(i -> System.out.println(i.contains("A")));}}
A forEach
loop can also be passed a method reference which specifies the function that can be applied to each element of the iterable on which it is invoked. For instance, the following line of code prints each element in the list:
list.forEach(System.out::println);
There are three types of variables in Java.
All three types of variables, when in scope, can be used in a lambda expression. However, the local variables can be used only if they are either explicitly declared final
or they undergo no change after their first assignment. So, for all practical purposes, they are treated by the compiler as effectively final. The lambda expression on lines 10–19 below use variables of all three kinds, but if we comment out line 22 the local variable does not remain effectively final, resulting in a compiler error.
class Main {static int staticVar = 1;boolean instanceFlag = true;public static void main(String[] args) {Main m = new Main();int localVar = 0;MyInterface lambda = () -> {System.out.println("Static: " +Main.staticVar +"\nInstance: " +m.instanceFlag +"\nLocal: " +localVar);};// If uncommented locaVariable does not remain effectively final and will produce a compiler error// localVar = 3;Main.staticVar = 10;m.instanceFlag = false;lambda.foo();}}@FunctionalInterfaceinterface MyInterface {void foo();}
Cloning an object means making a copy of that object. The Object
class defines a method clone()
that is inherited by all other classes since the Object
class is at the top of the inheritance hierarchy for Java. However, clone()
defined for the Object
class only makes shallow copies and must be overridden if deep copies are required.
A class whose instances are required to be cloned must implement the Cloneable
interface, otherwise, calling clone()
on such instances will throw the CloneNotSupported
exception.
In the following code, we show two classes Whole
and Part
that implement the Cloneable
interface. In the Part
class, we just call the clone()
function of the superclass (see line 12). On the other hand, the class Whole
contains a data member of type Part
. If we were to use the clone()
method of the superclass, it would make a copy of the address stored in the reference variable part
instead of making its deep copy. So we explicitly make a deep copy of part
on line 34.
class Part implements Cloneable {int id;// Part constructorPart(int id) {this.id = id;}@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone();}}class Whole implements Cloneable {String model;float price;Part part;// Whole constructorWhole(String model, float price, Part part) {this.model = model;this.price = price;this.part = part;}@Overridepublic Object clone() throws CloneNotSupportedException {// Calling super.clone() would makes a shallow copy of the Part objectWhole w = (Whole) super.clone();// All data members must be deep copiedw.part = (Part) this.part.clone();return w;}}class Main {public static void main(String[] args) {try {Whole w1 = new Whole("A", 10, new Part(4));Whole w2 = (Whole) w1.clone();w1.part.id = 3; // Modifying part.id in w1System.out.println(w1.model + " " + w1.price + " " + w1.part.id);System.out.println(w2.model + " " + w2.price + " " + w2.part.id);} catch (CloneNotSupportedException e) {System.out.println(e);}}}
A nested Java class that’s not static is called an inner class. An instance of an outer class must be created first in order to create an instance of an inner class, as shown on lines 34–35 in the widget below.
Inner classes are useful for organizing code with the aim of ensuring encapsulation.
// Outer class of Employeeclass Employee {String first = null;String last = null;// Employee constructorEmployee(String f, String l) {this.first = f;this.last = l;}// Inner class Dependent contained within the outer class Employeepublic class Dependent {String name = "";//Dependent constructorDependent(String n) {name = n;}// To print dependent informationvoid print() {System.out.println(name + " is a dependent of " + first + " " + last);}}}class Main {public static void main(String[] args) {Employee emp = new Employee("John", "Doe");Employee.Dependent dep1 = emp.new Dependent("Jane");Employee.Dependent dep2 = emp.new Dependent("Jack");dep1.print();dep2.print();}}
In the code above, we use the public
access modifier with the inner class. But just like for other data members, we could have also used the private
or protected
access modifiers with the inner class.
An anonymous class is a nested class that has no name and is defined and instantiated all at once. For this reason, an anonymous class can be instantiated only once. Note that an anonymous class is used to either extend another class or implement an interface. To create an anonymous class instance, the new
operator is applied to the name of its superclass or the name of the interface being implemented, as shown on lines 11–23 in the widget below.
interface MyInterface {public void foo();public void bar();}class Main {public static void main(String[] args) {// Creating an instance of an anonymous classMyInterface foobar = new MyInterface() {private String name = "foobar";@Overridepublic void foo() {System.out.println("Foo");}@Overridepublic void bar() {System.out.println("Bar");}};// Using the foobar objectfoobar.foo();foobar.bar();}}
An iterable is an object that implements the Iterable
interface. All Java collections implement this interface. This enables us to iterate over these collections using an iterator returned by the iterator()
method, as well as by using the forEach()
method.
An iterator, on the other hand, is an object that implements the Iterator
interface. It supports the hasNext()
method which can be used for checking if there is a non-null element at the next position in the collection. If the element exists, it can be retrieved using the next()
method. It also supports the remove()
method, which can be used for safely removing an element from the iterable during the iteration. The element removed is the same as the one returned by the previous call to next()
. The following code shows how these methods can be used on lines 15–18.
import java.util.ArrayList;import java.util.Iterator;class Main {public static void main(String[] args) {ArrayList<String> shapes = new ArrayList<>();shapes.add("Pentagon");shapes.add("Hexagon");shapes.add("Heptagon");shapes.add("Octagon");Iterator<String> it = shapes.iterator();String shape = "";while (it.hasNext()) {System.out.println(it.next());it.remove();}}}
Note that when an iterator is in use, direct additions or removals from a collection can result in a ConcurrentModificationException
exception being thrown.
The iterators that implement this behavior are called fail-fast iterators.
Both are Java collections used for storing key-value pairs. Internally, the HashMap
class is implemented like a hash table, but the TreeMap
class is implemented as a red-black tree, which is a balanced binary search tree. So retrieval, addition, and checking for containment of an element is for HashMap
vs. for TreeMap
. Searching is asymptotically the same at for both.
On the other hand, a tree map has a distinct advantage over a hash map in that the keys stored in it are ordered, and if an application has a need for ordering data, then it’s a better fit. The order in which the keys are stored is said to be the natural order and is specified by the implementation of compareTo
in the Comparable
interface. By default, the numeric keys in a tree map are stored under the usual ordering on the set of real numbers. Keys that are strings are ordered alphabetically.
The following example shows a few method calls that retrieve ordered data from a tree map.
import java.util.TreeMap;class Main {public static void main(String[] args) {TreeMap<String, Integer> treemap = new TreeMap<>();treemap.put("A", 1);treemap.put("Z", 5);treemap.put("B", 2);treemap.put("D", 4);treemap.put("C", 3);System.out.println("TreeMap: " + treemap);System.out.println("First entry: " + treemap.firstEntry()); // The "smallest" entrySystem.out.println("Last entry: " + treemap.lastEntry()); // The "greatest" entrySystem.out.println("Keys in descending order: " + treemap.descendingKeySet());System.out.println("Tail end of the map starting at C: " + treemap.tailMap("C"));System.out.println("Ceiling of M: " + treemap.ceilingKey("M")); // The "smallest" entry that's equal to or greater than "M"}}
The synchronized
keyword is applied to code blocks or methods to ensure that each such code block can be executed by only one thread at a time. Each code block is synchronized over an object shown as follows:
class Counter {
int count = 0;
void setCount() {
synchronized (count) {
// Code block synchronized over the variable count
}
}
}
The synchronized
keyword can also be applied directly to a method name. In such a case, the method is synchronized on the this
object. For instance, in the following code, the increment()
method defined on lines 6–16 is synchronized on the this
object.
class Counter {int count = 0;// A method synchronized on the this objectpublic synchronized void increment(String threadName) {System.out.println(this.count + " " + threadName);this.count++;// Monitor (lock) on this object is not released while this thread is suspendedtry {Thread.sleep(2000);} catch (InterruptedException e) {System.out.println(e);}return;}}class MyThread extends Thread {String name;Counter counter;// MyThread constructorMyThread(String n, Counter c) {name = n;counter = c;}@Overridepublic void run() {for (int i = 0; i <= 4; i++) {this.counter.increment(this.name);}}}class Main {public static void main(String[] args) {Counter c = new Counter();MyThread t1 = new MyThread("Ping", c);MyThread t2 = new MyThread("Pong", c);t1.start();t2.start();}}
A thread that enters the code of one of the synchronized methods has the lock over the this
object, and all other threads are blocked from entering and executing any method or code block synchronized on the same object.
Once the thread holding the lock exits the code block, the lock over the object is released.
If we remove the synchronized
keyword from line 6, we note that in one execution (see below), the first thread prints 0 Ping
, then the second thread prints 0 Pong
, followed by the first thread printing 2 Ping
. The fact that 0
was printed twice and 1
was never printed indicates that the threads were switched while increment()
was still being executed.
No. In a multithreaded environment, it’s possible that a variable is cached, and a write to the cached copy takes place by one thread while its older value is still visible and being read by other threads. Once the new value is updated in the main memory, only then does it become visible to other threads. To provide a consistent view to all threads, a variable is declared volatile
. A volatile variable is always updated in the main memory instead of the cache. But it does not ensure thread safety.
The interview questions above are by no means exhaustive, and are a reflection of a trajectory that can be taken by an interviewer as one question leads to another, sometimes related question.
At Educative, we offer a carefully curated array of interview courses. Some of these help improve problem-solving skills and strengthen basic foundations, while others help with language proficiency.
We wish you a happy journey and leave you with the following suggestions for your Java interview preparation:
It’s very common to have interviews testing your knowledge of a programming language you claim to be an expert in. If you've got an interview coming up on Java soon and need to get up to speed, this is the ideal course for you. You'll refresh your memory on everything from the basics to more advanced functionality you’re likely to be asked about. Even if you're using Java every day, you may not have been exposed to parts of it in some time, so it's always useful to make sure you're updated. This course features more than 300 of the most commonly asked core Java interview questions that you're likely to face as an experienced software engineer. You'll get to explore questions by topic, as well as see detailed answers for each one, and plenty of live code examples where relevant. Get started today.
Concurrency in Java is one of the most complex and advanced topics brought up during senior engineering interviews. Knowledge of concurrency and multithreading can put interviewees at a considerable advantage. This course lays the foundations of advanced concurrency and multithreading and explains concepts such as Monitors and Deferred Callbacks in depth. It also builds simple and complete solutions to popular concurrency problems that can be asked about in interviews like the Reader-Writer Problem and the Dining Philosopher Problem. While prior knowledge of concurrency is not required to follow through with this course, familiarity with the very basics of concurrency would be helpful.
Collection is one of the most important topics for Java programming. Without the proper knowledge of every collection type in Java, it becomes difficult for you to decide which option to pick in a particular scenario. This course will give you a great understanding of all collection types available in the Collections Framework like lists, linked lists, sets, and hashmaps. After completing this course, you will know how data can be modified within a collection, how to sort a collection, and how it can be made thread-safe. You will also cover the internal working of each Java collection so you can work with them more efficiently.
Free Resources