androidinterview.com

Java Interview Questions for Android Developers

35 questions

Tier
Difficulty
Level

Showing all 35 questions

OOP

These sound like textbook questions because they are. Answer them with an Android example and they stop sounding like one.

Explain OOP concepts.

Tier: EssentialDifficulty: Easy

Object-oriented programming is a way of structuring code around objects that bundle data and behavior together, and it rests on four pillars.

  • Encapsulation. An object hides its internal state behind a public interface. In Kotlin that's a class exposing methods or properties while keeping its fields private, so callers can't reach in and put it into an invalid state directly.
  • Abstraction. You expose what an object does without exposing how it does it. A Repository interface with a getUser() method hides whether that data comes from a network call, a database, or a cache.
  • Inheritance. A class can extend another and reuse its fields and behavior, adding or overriding what's specific to it. class SavingsAccount : Account() gets everything Account already does.
  • Polymorphism. The same call behaves differently depending on the actual object behind it. If Account has an open fun withdraw() and SavingsAccount overrides it, calling withdraw() on an Account reference at runtime runs SavingsAccount's version. That's runtime polymorphism. Overloading a function with different parameter types is compile time polymorphism.

The reason interviewers ask this is to see if you can connect the theory to real code, not recite definitions. In an Android app, encapsulation is why you expose a StateFlow from a ViewModel instead of a mutable one. Abstraction is why you code against a Repository interface instead of a concrete RetrofitRepository. Inheritance and polymorphism show up constantly in the Android framework itself, every Activity you write overrides onCreate(), which is polymorphism in action.

What are the differences between abstract classes and interfaces?

Tier: EssentialDifficulty: EasyAsked at: paytm

An abstract class is a partially built class you extend, an interface is a contract you implement, and that difference drives everything else.

An abstract class can hold state, constructors, and a mix of implemented and unimplemented methods. A class can only extend one abstract class, because Kotlin and Java don't allow multiple inheritance of implementation. You reach for it when you have a family of closely related types that genuinely share code, for example a BaseViewModel that all your ViewModels extend for common loading and error state handling.

An interface can't hold constructor logic or backing fields, though in Kotlin it can carry default method implementations and computed properties. A class can implement as many interfaces as it wants. You reach for it when you're describing a capability rather than an identity, for example Clickable or a Repository contract that several unrelated classes might implement.

abstract class BaseViewModel {
    protected val scope = viewModelScope
    abstract fun onLoad()
}

interface Repository {
    suspend fun getUser(id: String): User
}

The practical rule of thumb that tends to satisfy interviewers, favor interfaces for API design because they're flexible and testable, and use an abstract class only when you actually need to share real implementation or state across a family of subclasses.

What is the difference between method overloading and overriding?

Tier: EssentialDifficulty: Easy

Overloading is defining multiple methods with the same name but different parameters in the same class, resolved at compile time, while overriding is a subclass redefining a method it inherited with the exact same signature, resolved at runtime.

class Printer {
    void print(String s) { System.out.println(s); }
    void print(int n) { System.out.println(n); }        // overloading
}

class ColorPrinter extends Printer {
    @Override void print(String s) { System.out.println("Color: " + s); } // overriding
}

Overloading is about giving the same operation several entry points, print("x") and print(5) are picked apart purely by their parameter types, so the compiler decides which one you meant before the program even runs, that's compile time, or static, polymorphism. Overriding is about a subclass providing its own version of an inherited method, and which version actually runs is decided by the real object's type at runtime, that's runtime, or dynamic, polymorphism, and it's what makes Shape s = new Circle(); s.area() call Circle's implementation. The @Override annotation isn't required, but it's worth always using, it makes the compiler verify you're actually overriding something, rather than accidentally overloading it with a mismatched signature.

What access modifiers do you know? What does each one do?

Tier: CommonDifficulty: Easy

Java has four access levels, from strictest to most open.

  • private, visible only inside the containing class. This is the default you should reach for, used to hide a field behind getters and setters or keep an implementation detail out of reach.
  • default, or package-private, no keyword needed. Visible to any class in the same package, but not to subclasses in a different package.
  • protected, visible to the same package plus any subclass, even in a different package. Useful for members a base class wants to expose only to its own hierarchy.
  • public, visible everywhere. Reserved for the actual API surface a class wants the rest of the app to depend on.

A useful habit is to start every member private and only widen it when something outside the class genuinely needs access, that keeps your public API small and your internals free to change. A singleton's constructor being private is a common concrete example, it's the access modifier that actually enforces there's only one instance. Kotlin has the same four ideas but renames the package-private default to internal, which scopes to the whole module rather than a package, and makes public the actual default if you write nothing at all.

What is polymorphism? What about inheritance?

Tier: CommonDifficulty: Easy

Inheritance is one class acquiring the fields and methods of another, and polymorphism is the ability to call the same method name on different types and get behavior specific to each one.

class Shape {
    double area() { return 0; }
}
class Circle extends Shape {
    double radius;
    @Override double area() { return Math.PI * radius * radius; }
}

Shape s = new Circle();
s.area(); // calls Circle's version, decided at runtime

Circle extends Shape is inheritance, Circle gets everything Shape has and can add or override its own. Calling s.area() is polymorphism, the compiler only knows s is a Shape, but the JVM looks at the actual object at runtime and dispatches to Circle.area(). That's runtime polymorphism, driven by method overriding. There's also compile time polymorphism, method overloading, where the compiler picks between several methods with the same name based on the arguments you pass. Inheritance is what makes runtime polymorphism possible in the first place, without a shared supertype like Shape, there'd be no common reference type to call area() through.

Collections & Generics

What is the difference between Arrays and ArrayLists?

Tier: CommonDifficulty: Easy

An array has a fixed size set at creation and can hold primitives directly, while an ArrayList resizes itself as you add or remove elements but can only hold objects.

  • Size. An array's length is fixed forever once created. An ArrayList grows and shrinks automatically as you call add() and remove().
  • Element types. An array can hold primitives directly, int[], double[], with no boxing. An ArrayList can only hold objects, so ArrayList<Integer>, not ArrayList<int>, meaning every primitive you store gets autoboxed.
  • API. An array only gives you .length and index access. An ArrayList gives you a full API, add(), remove(), contains(), indexOf(), and everything else from the List interface.
  • Performance. Arrays are slightly faster and use less memory for primitives, since there's no boxing and no resizing overhead. An ArrayList pays a small cost for both, but that cost is rarely the bottleneck in real code.
int[] fixed = new int[3];
ArrayList<Integer> flexible = new ArrayList<>();
flexible.add(1);
flexible.add(2);

The practical rule, reach for an array only when the size is genuinely fixed and you need raw primitive performance, otherwise ArrayList, or List in general, is the default because the flexibility almost always outweighs the small overhead.

Explain Generics in Java.

Tier: CommonDifficulty: Medium

Generics let you write a class or method once and have it work with any type, while still catching type mistakes at compile time instead of with a ClassCastException at runtime.

class Box<T> {
    private T value;
    void set(T value) { this.value = value; }
    T get() { return value; }
}

Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // no cast needed

Before generics, a container like this would just store Object, and every read required an explicit cast that the compiler couldn't verify, so a wrong type only blew up when the code actually ran. Box<String> fixes that, the compiler enforces T is String everywhere the box is used.

Under the hood the JVM doesn't actually know about T at runtime, generics are erased during compilation, a process called type erasure. Box<String> and Box<Integer> both compile down to the same Box class using Object internally, with the compiler inserting the casts for you. That's why you can't do things like new T() or check instanceof T inside a generic class, the type information simply isn't there anymore once compiled.

What is the difference between fail-fast and fail-safe iterators in Java?

Tier: CommonDifficulty: Medium

A fail-fast iterator throws a ConcurrentModificationException the moment it detects the underlying collection was structurally changed while you were iterating, while a fail-safe iterator quietly works on a separate copy of the data and never throws, even if the original collection changes underneath it.

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
    list.remove(s); // throws ConcurrentModificationException on the next iteration
}

Map<String, String> safeMap = new ConcurrentHashMap<>();
for (String key : safeMap.keySet()) {
    safeMap.remove(key); // no exception, iterates over a stable snapshot
}

ArrayList, HashMap, and HashSet are all fail-fast, they track a modification count internally, and if the iterator notices that count changed mid loop, it throws immediately rather than let you silently corrupt your traversal. CopyOnWriteArrayList and ConcurrentHashMap are fail-safe, they iterate over a snapshot taken when iteration started, so changes made by another thread during the loop just don't show up in that pass. Fail-safe isn't automatically better, that snapshot means the iterator can hand you stale data, and copying the collection has its own cost, it's the right tool specifically for concurrent access, not a general replacement for the regular collections.

Strings & Objects

What is the difference between String, StringBuffer and StringBuilder?

Tier: EssentialDifficulty: Easy

String is immutable, StringBuffer and StringBuilder are both mutable, and the only difference between the two mutable ones is that StringBuffer is thread safe and StringBuilder isn't.

String s = "a" + "b" + "c";      // three intermediate String objects created

StringBuilder sb = new StringBuilder();
sb.append("a").append("b").append("c"); // one buffer, mutated in place

Every concatenation on a String allocates a brand new object, since the original can never change, so building a string in a loop with + gets expensive fast. StringBuilder avoids that, it holds an internal, resizable character buffer and mutates it directly, which makes it dramatically faster for repeated appends in ordinary, single threaded code, the compiler even rewrites simple + concatenation into StringBuilder calls automatically. StringBuffer does the exact same job but wraps every method in synchronized, so it's safe to share across threads, at the cost of locking overhead you're paying for even when only one thread ever touches it. The rule of thumb, use String for values that shouldn't change, StringBuilder for building strings on a single thread, which is nearly always, and reach for StringBuffer only in the rare case multiple threads genuinely mutate the same buffer concurrently.

What is the difference between using == and .equals() on an object?

Tier: EssentialDifficulty: Easy

== compares references by default, .equals() compares whatever a class defines as meaningful equality, and those are only the same thing if the class never overrides equals().

In Kotlin this trips people up because == is actually structural equality, it compiles down to calling .equals() for you. So == and .equals() behave the same way in Kotlin for regular classes. === is the one that checks reference identity, whether two variables point at the literal same object in memory. That's the opposite of Java, where == is always reference identity and you have to call .equals() yourself to get structural comparison.

data class User(val id: String)

val a = User("1")
val b = User("1")

a == b   // true, data class generates a structural equals()
a === b  // false, two different objects in memory

The reason this matters in practice, if a class doesn't override equals(), it falls back to the default from Any or Object, which is reference identity anyway. So == only feels different from === for classes that have deliberately defined what "equal" means, like a data class, which generates equals() and hashCode() from its constructor properties automatically.

What are anonymous classes?

Tier: CommonDifficulty: Easy

An anonymous class is a class with no name that you define and instantiate in a single expression, usually to provide a one-off implementation of an interface or an abstract class.

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("TAG", "clicked");
    }
});

There's no separate class file and no name to reuse elsewhere, you're implementing OnClickListener right at the call site, once, for this one button. It's the classic pattern from pre-lambda Java, and it's still how you implement a Java interface with more than one abstract method, or one that needs a bit of extra setup, inline.

Kotlin replaces most of this with lambdas for functional interfaces, and with object : SomeInterface { } expressions when you need to implement several methods at once, both are more concise than Java's anonymous class syntax but solve the exact same problem.

What does it mean to say that a String is immutable?

Tier: CommonDifficulty: Easy

It means once a String object is created, its content can never change, any method that looks like it modifies a string is actually building and returning a brand new one.

String greeting = "hello";
greeting.toUpperCase();       // return value discarded, greeting is still "hello"
greeting = greeting.toUpperCase(); // now greeting points at a new String, "HELLO"

Calling toUpperCase() on the first line doesn't touch greeting at all, it computes a new String and hands it back, and since nothing captures that return value, it's just discarded. You have to reassign the variable yourself to see the effect. This trips people up constantly with methods like trim(), replace(), and substring(), they all follow the same rule, no method on String ever mutates the object it's called on.

What is the difference between an Integer and an int?

Tier: CommonDifficulty: Easy

int is a primitive, a raw 32 bit number stored directly on the stack or inline in an object, while Integer is a full object on the heap that wraps an int and adds useful methods on top, like parseInt() and compare().

int a = 5;
Integer b = 5;       // autoboxed into Integer.valueOf(5)
Integer c = 5;
b == c;               // true, both hit the cached Integer for small values
Integer d = 200;
Integer e = 200;
d == e;               // false, outside the cache range, two distinct objects

The compiler autoboxes between the two for you, so most code doesn't notice the difference day to day. But Integer caches instances for values from -128 to 127, so == happens to work for small numbers and quietly breaks for larger ones, which is exactly why you should always compare Integer values with .equals(), never ==. The other place this matters is generics, List<int> isn't legal, generics only work with reference types, so List<Integer> is what you actually write, and every read or write pays a small autoboxing cost. Kotlin's Int looks primitive everywhere but the compiler silently boxes it to the same Integer whenever it needs to go into a generic collection or become nullable.

Do objects get passed by reference or by value in Java? Elaborate.

Tier: CommonDifficulty: Medium

Everything in Java is passed by value, there is no pass by reference. What confuses people is that for an object, the value being copied is the reference itself, not the object.

void rename(User user) {
    user.name = "changed";   // mutates the shared object, visible outside
    user = new User("new");  // reassigns the local copy only, invisible outside
}

Calling rename(myUser) copies the reference into the parameter user. Both the caller's variable and the parameter now point at the same object on the heap, so mutating a field through user is visible to the caller. But reassigning user to point at a brand new object only changes the local copy of the reference, the caller's original variable still points at the original object. That's the whole trick, you can mutate what's pointed to, but you can never make the caller's variable point somewhere else.

Explain the String Pool in Java.

Tier: CommonDifficulty: Medium

The String Pool is a special region of the heap where the JVM stores string literals, and it reuses an existing entry instead of creating a new object whenever it sees the same literal text twice.

String a = "hello";
String b = "hello";
String c = new String("hello");

a == b        // true, both point at the same pooled literal
a == c        // false, new String() forces a fresh heap object
a == c.intern() // true, intern() looks the value up in the pool

Because String is immutable, sharing one object across many variables is completely safe, nothing can mutate "hello" out from under another reference. That's the whole point of the pool, strings are one of the most heavily allocated types in any program, so deduplicating identical literals saves real memory. new String("hello") deliberately skips the pool and allocates on the regular heap, which is why == on it returns false even though .equals() would return true. You can force a string back into the pool with intern(), but in normal code you should almost never compare strings with ==, always use .equals().

How is the String class implemented? Why was it made immutable?

Tier: CommonDifficulty: Medium

String is backed internally by a final character array, historically char[], and since Java 9 a more compact byte[] with an encoding flag, and it was made immutable on purpose, mainly for safety, caching, and sharing.

String s1 = "hello";
String s2 = s1.toUpperCase(); // creates and returns a new String, s1 is untouched

Every method that looks like it modifies a string actually builds and returns a new one, the original object never changes after construction. That immutability is what makes the String Pool possible, the JVM can safely hand out the same pooled object to multiple variables because nothing can mutate it out from under another reference. It's also why String is a safe key for a HashMap, its hashCode() is computed once and cached, since the value can never change, and it's why strings are the classic type to hold sensitive data like a hostname or a class name, an attacker can't rewrite a string in flight the way they could a mutable buffer. The trade off is every concatenation or transformation allocates a new object, which is exactly why StringBuilder exists for cases with heavy, repeated string building.

What are hashCode() and equals() used for?

Tier: CommonDifficulty: Medium

equals() decides whether two objects are logically the same, and hashCode() gives an object a numeric fingerprint, and hash based collections like HashMap and HashSet need both working together correctly to find and deduplicate objects efficiently.

class Point {
    int x, y;
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

A HashMap uses hashCode() first to jump straight to the right bucket, then uses equals() to confirm the object it finds there is actually a match, rather than scanning every entry. The contract that makes this work, if two objects are equals(), they must return the same hashCode(). The reverse isn't required, two unequal objects can share a hash code, that's just a collision the map handles internally. Break that contract, override equals() but forget hashCode(), and a Point(1, 2) you just put into a HashSet can come back as "not found" when you look it up with an equal but different object, because the set went looking in the wrong bucket entirely.

What is the difference between a shallow copy and a deep copy in Java?

Tier: CommonDifficulty: Medium

A shallow copy duplicates an object but reuses references to whatever it points to, so the copy and the original still share the same nested objects, while a deep copy recursively duplicates those nested objects too, so the two are fully independent.

class Address { String city; }
class User implements Cloneable {
    String name;
    Address address;
    User shallowCopy() { return (User) this.clone(); } // same Address reference
    User deepCopy() {
        User copy = shallowCopy();
        copy.address = new Address();
        copy.address.city = this.address.city; // separate Address object
        return copy;
    }
}

With the shallow copy, original.address and copy.address point at the exact same Address object, so mutating one is visible through the other, which usually surprises people the first time they hit it. Java's default Object.clone() only does a shallow copy, it copies primitive fields by value and reference fields by reference, nothing more. Getting a real deep copy means writing it yourself, field by field, or leaning on a library that serializes and reconstructs the whole object graph. The rule of thumb, if an object only holds primitives and immutable types like String, a shallow copy already behaves like a deep one, it's only mutable nested objects that make the distinction matter.

Exceptions

How do try{}, catch{} and finally work?

Tier: CommonDifficulty: Easy

try wraps code that might throw, catch handles a specific exception type if one is thrown, and finally runs no matter what happened, whether the try block succeeded, threw, or even returned.

try {
    return riskyCall();
} catch (IOException e) {
    log(e);
    return null;
} finally {
    connection.close(); // always runs, even after the return above
}

If riskyCall() succeeds, its return value is remembered, finally runs, and only then does the method actually return. If it throws an IOException, the matching catch block runs instead, and finally still runs afterward. You can chain multiple catch blocks for different exception types, and the first one that matches wins. finally is where you put cleanup that has to happen either way, closing a stream, releasing a lock, or closing a database cursor.

One gotcha worth knowing, if finally itself contains a return, it silently overrides any return value or exception from the try or catch block, which is exactly why you should never put a return inside finally.

What is the difference between a checked exception and an unchecked exception?

Tier: CommonDifficulty: Easy

A checked exception is one the compiler forces you to handle, either catch it or declare it with throws, while an unchecked exception can be thrown without any such requirement, and the compiler won't stop you if you ignore it.

void readFile() throws IOException {   // checked, must be declared or caught
    new FileReader("data.txt");
}

void divide(int a, int b) {
    int result = a / b; // unchecked, ArithmeticException, no declaration needed
}

Checked exceptions extend Exception but not RuntimeException, IOException and SQLException are the usual examples, they represent conditions a well written program can reasonably anticipate and recover from, like a missing file or a dropped connection. Unchecked exceptions extend RuntimeException, NullPointerException, ArithmeticException, IllegalArgumentException, they usually represent programming bugs rather than expected failure modes, and forcing every caller to declare or catch them everywhere would just add noise. Kotlin drops the distinction entirely, every exception is unchecked, which is why calling Java code that throws a checked exception from Kotlin doesn't require any try/catch at all.

Language Features

What are annotations?

Tier: CommonDifficulty: Easy

Annotations are metadata you attach to code, a class, method, field, or parameter, that describe something about it without changing what the code actually does when it runs.

@Override
public String toString() { return "User"; }

@Deprecated
public void oldMethod() { }

@Override doesn't add behavior, it just tells the compiler to verify this method really does override a superclass method, and flag a typo as an error instead of silently creating a brand new method. @Deprecated doesn't stop anyone from calling the method, it just warns them not to. Beyond the handful built into the language, annotations become genuinely powerful once a tool reads them, Room reads @Entity and @Dao to generate database code at compile time, Retrofit reads @GET and @Body to build HTTP requests, and Dagger reads @Inject and @Module to wire up your dependency graph. In every case the annotation itself is inert, all the real work happens in whatever processes it, whether that's the compiler, an annotation processor, or reflection at runtime.

What are the final, finally and finalize keywords?

Tier: CommonDifficulty: Easy

They look alike but do three unrelated things.

  • final marks something as unmodifiable. On a variable, it can only be assigned once. On a method, it can't be overridden by a subclass. On a class, it can't be extended at all, String and Integer are both final classes.
  • finally is a block attached to try and catch that always runs afterward, success, exception, or even an early return, and is where you put cleanup like closing a stream or a connection.
  • finalize() is a method the garbage collector used to call on an object right before reclaiming its memory, meant as a last chance to release resources. It's deprecated and effectively dead in modern Java, its timing was never guaranteed, it could hurt GC performance, and it might never run at all. try-with-resources and AutoCloseable are the actual answer to cleanup today.

The only thing these three genuinely share is the first five letters and a habit of showing up in the same interview question.

What does the static keyword mean in Java?

Tier: CommonDifficulty: Easy

static means a member belongs to the class itself, not to any individual instance, so there's exactly one copy of it shared across every object of that class.

class Counter {
    static int totalCreated = 0; // shared across all instances
    Counter() { totalCreated++; }

    static Counter create() { return new Counter(); } // no instance needed to call this
}

A static field is shared state, changing it through one instance changes what every other instance sees, which is exactly why it's the classic place bugs creep in if you're not careful about threading. A static method can be called without ever constructing an object, Counter.create(), not myCounter.create(), which is why it's the natural home for factory methods and utility functions that don't need any instance state. Kotlin doesn't have a static keyword at all, it uses a companion object inside a class for the same purpose, or top level functions for pure utilities that don't need a class wrapper.

Can a static method be overridden in Java?

Tier: CommonDifficulty: Medium

No. A subclass can declare a static method with the same signature, but that's hiding, not overriding, and the two behave very differently.

Overriding is resolved at runtime based on the actual object type. Hiding is resolved at compile time based on the reference type you're calling through. That difference shows up the moment you call the method through a parent class reference.

class Animal {
    static void speak() { System.out.println("Animal"); }
}
class Dog extends Animal {
    static void speak() { System.out.println("Dog"); }
}

Animal a = new Dog();
a.speak(); // prints "Animal", not "Dog"

With a real instance method, a.speak() would print "Dog", because the JVM looks at the object's actual class. With a static method, the compiler only looks at the declared type of the reference, Animal, and binds the call to Animal.speak() before the program even runs. That's the classic interview trap, static methods belong to the class, not to any instance, so polymorphism never enters the picture.

Explain Reflection in Java.

Tier: CommonDifficulty: Medium

Reflection is the ability to inspect and manipulate classes, methods, and fields at runtime, even ones you don't have compile time access to, using the java.lang.reflect API.

Class<?> clazz = Class.forName("com.example.User");
Method method = clazz.getDeclaredMethod("getName");
method.setAccessible(true);
Object result = method.invoke(userInstance);

You can list a class's fields and methods, read or set a private field by bypassing its access modifier, call a method by name, or construct an instance without ever writing new. It's how a lot of frameworks work behind the scenes, Gson uses it to map JSON fields onto your model's fields, Retrofit and Room use it to read annotations off your interfaces and entities, and dependency injection frameworks use it to construct and wire objects they've never seen at compile time.

The cost is real though. Reflective calls are noticeably slower than direct calls because the JVM can't inline or optimize them the usual way, and you lose compile time safety, a typo in a method name only fails at runtime. It's also why heavily reflection based libraries feel slow to start up on Android, which is part of why Room and Moshi's KSP variants generate real code at compile time instead of reflecting at runtime.

Explain serialization and deserialization in Java. How do you implement it?

Tier: CommonDifficulty: Medium

Serialization is converting an object's state into a byte stream so it can be saved to disk or sent over a network, and deserialization is rebuilding an equivalent object from that byte stream later.

You implement it by having your class implement the Serializable marker interface, which has no methods, it just tells the JVM this class is allowed to be serialized.

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    transient String sessionToken; // excluded from serialization
}

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.dat"));
out.writeObject(user);

ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.dat"));
User restored = (User) in.readObject();

serialVersionUID is a version identifier for the class, if you deserialize an object with an older or newer version than the one the JVM has loaded, mismatched IDs throw an InvalidClassException, so it's worth declaring it explicitly rather than letting the compiler generate one for you. Any field marked transient is skipped entirely, which is exactly where you'd exclude something like a password or a session token that shouldn't be persisted. On Android, you'd rarely use Serializable directly, Parcelable is the faster, Android specific alternative for passing data between components, and Gson or Moshi handle JSON serialization for network and storage.

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

OOP

Can an interface implement another interface?

Tier: Less commonDifficulty: Easy

Not quite, an interface can't implement another interface, but it can extend one, or several, using extends instead of implements.

interface Named {
    String getName();
}
interface Aged {
    int getAge();
}
interface Person extends Named, Aged {
    String describe();
}

Person doesn't provide bodies for getName() or getAge(), it just inherits their contracts and adds its own. Any class that implements Person has to provide all three methods. This is one of the places interfaces are more flexible than classes, a class can only extend one superclass, but an interface can extend as many interfaces as it wants, since there's no implementation to conflict.

Collections & Generics

What is the difference between HashMap and Set?

Tier: Less commonDifficulty: Easy

A HashMap stores key-value pairs, while a Set, specifically HashSet, stores unique elements with no values attached, and the two aren't really competitors, HashSet is actually built on top of HashMap internally.

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30); // key maps to a value

Set<String> names = new HashSet<>();
names.add("Alice");    // just a unique element, no associated value

Look at the JDK source and HashSet literally wraps a HashMap, every element you add becomes a key in a backing map, paired with a dummy constant value that's never used. So a HashSet gives you HashMap's exact performance characteristics, average O(1) add, remove, and contains, because under the hood it's the same hashing and bucketing doing the work. The real distinction is what problem you reach for, use HashMap when you need to look something up by a key, use HashSet when all you care about is whether a value exists in the collection at all, with no data attached to it.

What is the difference between HashSet and TreeSet?

Tier: Less commonDifficulty: Easy

HashSet gives you fast, unordered storage of unique elements, while TreeSet keeps its elements sorted at all times, at the cost of slower operations.

Set<Integer> hash = new HashSet<>(List.of(5, 1, 3));
System.out.println(hash);   // order not guaranteed, e.g. [1, 3, 5] or any order

Set<Integer> tree = new TreeSet<>(List.of(5, 1, 3));
System.out.println(tree);   // always [1, 3, 5], sorted

HashSet is backed by a HashMap, so add(), remove(), and contains() are average O(1), and it makes no promise about iteration order at all. TreeSet is backed by a red-black tree, so those same operations are O(log n), slower, but you always get elements back in sorted order, and it adds range operations like first(), last(), and headSet() that a HashSet simply can't offer. There's also a middle ground worth knowing, LinkedHashSet, which keeps HashSet's O(1) performance but preserves insertion order instead of sorted order. Pick HashSet by default for raw speed, reach for TreeSet only when you actually need the elements sorted.

Strings & Objects

Can you list the 8 primitive types in Java?

Tier: Less commonDifficulty: Easy

Yes, Java has exactly eight primitive types, and every one of them stores its value directly rather than as a reference to an object.

  • byte, an 8 bit signed integer.
  • short, a 16 bit signed integer.
  • int, a 32 bit signed integer.
  • long, a 64 bit signed integer.
  • float, a 32 bit floating point number.
  • double, a 64 bit floating point number.
  • char, a single 16 bit Unicode character.
  • boolean, true or false.

Everything else in Java, String, Integer, your own classes, is a reference type that lives on the heap. Kotlin hides this distinction on the surface, you write Int and Boolean regardless, and the compiler decides whether to compile them down to JVM primitives or box them into objects depending on context, like when you put an Int into a generic collection.

Exceptions

What is the difference between the throw and throws keywords in Java?

Tier: Less commonDifficulty: Easy

throw actually raises a specific exception instance right where you write it, while throws just appears in a method signature to declare which checked exceptions that method might raise, without raising anything itself.

void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) {
        throw new InsufficientFundsException("Not enough balance"); // throw: fires now
    }
}

throws is purely a compile time declaration, it's how a method warns its callers, this can fail with this checked exception, so handle it or declare it yourself. It never appears more than once per method, but it can list several exception types separated by commas. throw is a runtime statement, it takes one exception object and hands control to the nearest matching catch block, or up the call stack if there isn't one locally. You'll only ever see one throw fire per call, but a method can contain many throw statements for different conditions.

Language Features

What is the transient modifier?

Tier: Less commonDifficulty: Easy

transient marks a field so Java's default serialization skips it entirely, the field simply isn't written to the byte stream, and comes back null, or its type's default value, when the object is deserialized.

class User implements Serializable {
    String name;
    transient String sessionToken; // never written to disk or the network
}

It's the right tool for fields that are either sensitive, like a password or an auth token you don't want persisted, or that can't meaningfully survive serialization at all, like an open Socket or a Thread, which have no sane representation as bytes. It only affects Java's built in Serializable mechanism, it has no effect on other serialization approaches like Gson or Moshi turning an object into JSON, those use their own annotations, like Gson's @Transient or Moshi's @Json(ignore = true), to say the same thing for their own format.

When is a static block run?

Tier: Less commonDifficulty: Easy

A static block runs exactly once, the very first time its class is loaded by the JVM, whether that's triggered by creating the first instance or by accessing a static member for the first time.

class Config {
    static final String API_URL;
    static {
        API_URL = loadFromEnvironment(); // runs once, before any instance exists
    }
}

It runs before any constructor, and before any instance is created, because it's part of class initialization, not object creation. If a class has multiple static blocks, they run in the order they're written, top to bottom. This is the natural place for one time setup that can't be expressed as a simple field initializer, computing a value that needs a few lines of logic, loading configuration, or validating some invariant about the class before it's usable. Kotlin has no direct equivalent, an init block inside a companion object is the closest match, since a companion object is itself a singleton initialized once, the first time the class is touched.

When would you make an object value final?

Tier: Less commonDifficulty: Easy

Mark a variable final whenever it's only ever assigned once, which is most of the time, it documents intent, catches accidental reassignment at compile time, and makes the code easier to reason about since you know that reference can never change.

final List<String> names = new ArrayList<>();
names.add("a"); // fine, the contents can still change
names = new ArrayList<>(); // compile error, the reference itself is locked

It's worth being precise about what final actually locks. On an object reference, it only prevents reassigning the variable to point at something else, it says nothing about the object's own mutable state, names above is final but its contents are still perfectly mutable. For genuine immutability you need the object's own fields to be final and unexposed too. Beyond documenting intent, final fields on an object matter for concurrency, a properly constructed object with all final fields is guaranteed by the Java Memory Model to be safely visible across threads without extra synchronization, which is one of the strongest reasons to default to it. It's also required for local variables captured inside a lambda or an anonymous class, they have to be final, or effectively final, meaning never reassigned even without the keyword.

How do you create a custom annotation?

Tier: Less commonDifficulty: Medium

You declare it with @interface, then use two meta annotations to control where it can be applied and how long it sticks around.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Loggable {
    String tag() default "APP";
}

@Loggable(tag = "NETWORK")
public void fetchData() { /* ... */ }

@Target restricts where the annotation can be used, METHOD, FIELD, TYPE, and so on. @Retention decides how far it survives, SOURCE means it's stripped after compilation and is typically used by annotation processors like Room's, CLASS means it's kept in the bytecode but invisible at runtime, and RUNTIME means it's readable via reflection while the app is running, which is what a library like Gson or Retrofit needs to inspect your annotations on the fly.

Reading it back is just reflection.

Method method = MyClass.class.getMethod("fetchData");
Loggable annotation = method.getAnnotation(Loggable.class);
String tag = annotation.tag(); // "NETWORK"

This pattern, a custom annotation plus reflection or an annotation processor, is exactly how libraries like Room's @Entity, Retrofit's @GET, and Moshi's @Json work, they're just metadata until some tool decides to act on them.