Java interviews for freshers usually test whether you can explain the basics in your own words: OOP, strings, the common keywords, collections you use every day, exceptions and simple threads. Then comes a small program on a whiteboard and a few questions about what you built in college. It is written for final-year students, new graduates and anyone coming out of an internship or a bootcamp who is facing a first Java interview. Each question shows what the interviewer is checking, the shape of a good answer and a short answer you can say out loud. Practice the programs by hand, then swap the project stories for your own.
Search all questions by round, difficulty and level, or save the ones you want to practice.
Pick one example: a class from your own project, named up front.
Walk the four: encapsulation, inheritance, polymorphism, abstraction, each tied to that example.
Why it helped: one line on what the design made easier.
“In my library management project I had an abstract class called Member. Encapsulation: the fine balance was a private field, and the only way to change it was through methods like addFine and payFine, so nobody could set it to a negative number. Inheritance: Student and Faculty both extended Member and reused its code. Polymorphism: each subclass overrode getBorrowLimit, so when the checkout code called member.getBorrowLimit it got the right number without checking the type. Abstraction: the checkout screen only knew about Member and its public methods, not how each type worked inside. The nice part was that when we added a Guest member late in the project, the checkout code didn't change at all.”
Reciting textbook definitions with no example, or mixing up abstraction and encapsulation.
Constructor: same name as the class, no return type, runs on new.
this(): calls another constructor in the same class.
super(): calls the parent's constructor, so the parent part is built first; classically it's the first line.
“A constructor is a special block with the same name as the class and no return type, and it runs when I write new to set up the object's starting state. If I don't write any, Java gives me a default no-argument one, but only if I've written none at all. this() lets one constructor call another in the same class, which avoids repeating code: a two-argument constructor can pass a default value to the three-argument one. super() calls the parent class's constructor. If I don't write it, Java inserts a call to the parent's no-argument constructor for me, so the parent part of the object is always built before the child part. Traditionally both this() and super() have to be the first statement, so you can't use both in one constructor.”
class Account {
String owner;
double balance;
Account(String owner) {
this(owner, 0.0); // reuse the other constructor
}
Account(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
}
Giving a constructor a return type, or not knowing that the parent constructor runs first.
private: inside the same class only.
default (no keyword): anywhere in the same package.
protected: same package, plus subclasses in other packages.
public: everywhere.
“From tightest to loosest: private means only code inside the same class can see it, which is what I use for fields. Default, which is what you get when you write no modifier at all, means anything in the same package can see it; people call it package-private. protected includes the same package and also subclasses, even when they're in a different package. public means any class anywhere can use it. My rule in college projects was to make fields private and expose only the methods other classes really needed. A detail that surprises people is that protected is actually wider than default, not narrower, because it adds subclasses on top of the package.”
Thinking default means public, or that protected is only visible to subclasses.
Diamond problem: two parents with the same method, which one does the child get?
Java's choice: one parent class, many interfaces.
Default clash: the compiler forces the class to override and pick, using InterfaceName.super.
“If a class could extend two classes that both define the same method, or both carry state, it would be unclear which version the child inherits. That's the diamond problem, and Java avoids it by allowing only one parent class. A class can implement many interfaces, and for a long time that was safe because interfaces had no method bodies. Default methods brought the clash back: if two interfaces both give a default hello method, a class implementing both won't compile until it overrides hello itself. Inside that override it can pick one side with First.super.hello(), or combine both. Also, if a parent class already has a hello method, the class's version wins over any interface default.”
interface First { default String hello() { return "first"; } }
interface Second { default String hello() { return "second"; } }
class Both implements First, Second {
@Override
public String hello() {
return First.super.hello(); // must choose explicitly
}
}
Saying interfaces give full multiple inheritance with no conflicts, or not knowing the code fails to compile.
| Shallow | a new outer object whose fields still point to the same inner objects. |
|---|---|
| Deep | the inner mutable objects are copied too, so nothing is shared. |
How: a copy constructor that copies each mutable field; be careful with clone.
“A shallow copy creates a new outer object but copies each field as it is, so if a field is a reference to a list, both copies point to the same list. Add a mark to one student's list and the other student's list changes too. A deep copy also copies the inner mutable objects, so the two are fully independent. Object's clone method does a shallow copy by default, and it only works if the class implements Cloneable, otherwise it throws CloneNotSupportedException, which makes it awkward. I'd rather write a copy constructor that creates a new list with the same elements. Immutable fields like Strings can be shared safely, so only the mutable ones need copying.”
class Student {
String name;
List<Integer> marks;
Student(String name, List<Integer> marks) {
this.name = name;
this.marks = new ArrayList<>(marks);
}
Student(Student other) { // deep copy
this.name = other.name; // String is immutable, safe to share
this.marks = new ArrayList<>(other.marks);
}
}
Thinking assignment (b = a) makes a copy, or that clone always makes a deep copy.
Compile: javac turns .java source into .class files holding bytecode.
Run: a JVM built for each operating system reads that same bytecode.
Catch: the program is portable; the JVM itself is not.
“When I compile a Java file with javac, it doesn't produce machine code for my laptop. It produces a .class file full of bytecode, which is a set of instructions for the Java Virtual Machine rather than for a real processor. Every operating system has its own JVM, and each one knows how to run that same bytecode, interpreting it and compiling the hot parts to native code as it goes. So I can compile on Windows and run the same .class file on Linux or a Mac without changing anything. That's what platform independent means. The small catch is that the JVM itself is platform dependent: you install a different one for each system, which is exactly what lets the program stay the same.”
Saying the JVM is platform independent, or that javac produces an executable for the current machine.
Word by word: public, static, void, main, String[] args.
Why static: the JVM calls it before any object exists.
args: the command-line arguments, as an array of strings.
“public means the JVM, which sits outside my class, is allowed to call it. static means the method belongs to the class, not to an object, so the JVM can call it without first creating an instance. void means it returns nothing to the caller. main is simply the name the launcher looks for as the starting point. And String[] args holds whatever I type after the class name on the command line, so java App hello world gives me an array of two strings. In the classic form, static is needed because at the moment the program starts, no object of my class exists yet. Newer Java releases also accept a simpler main method, but interviewers usually mean this classic version.”
public class App {
public static void main(String[] args) {
System.out.println("Got " + args.length + " arguments");
}
}
Saying static means the value can't change, or not knowing what args contains.
Rule: Java is always pass-by-value.
With objects: the value copied is the reference, so both point to the same object.
Proof: changing a field shows up; reassigning the parameter doesn't.
“Java is always pass-by-value. With a primitive, the method gets a copy of the number, so changing it inside the method doesn't affect the caller. With an object, the variable holds a reference, and it's that reference that gets copied. So the method's parameter and the caller's variable point to the same object. If the method changes a field through that reference, the caller sees the change, which is why people think it's pass-by-reference. But if the method reassigns the parameter to a new object, only its local copy moves; the caller's variable still points to the original. In this example the dog ends up called Max, not Rex. With true pass-by-reference, the reassignment would have changed the caller too.”
class Dog {
String name;
Dog(String name) { this.name = name; }
}
static void rename(Dog d) {
d.name = "Max"; // caller sees this
d = new Dog("Rex"); // caller does not
}
// Dog pet = new Dog("Tom");
// rename(pet);
// pet.name is now "Max"
Saying objects are passed by reference and primitives by value, without being able to explain the reassignment case.
Wrappers: object versions of primitives, needed for collections and null.
Autoboxing: the compiler converts between int and Integer for you.
The trap: small values come from a cache, big ones are new objects, so == is unreliable.
“Every primitive has a wrapper class, like int and Integer or double and Double. We need them because collections only hold objects, so a List can't store a plain int, and because a wrapper can be null. Autoboxing is the compiler turning an int into an Integer for me, using Integer.valueOf, and unboxing is the reverse. The == puzzle comes from that valueOf call: Java caches Integer objects from minus 128 to 127, so two boxed 100s are literally the same object and == is true. Two boxed 1000s are usually separate objects, so == compares references and gives false. The fix is simple: compare wrapper objects with equals, or unbox to int first. Also, unboxing a null Integer throws a NullPointerException.”
Integer a = 100, b = 100;
Integer c = 1000, d = 1000;
System.out.println(a == b); // true: cached object
System.out.println(c == d); // false: two objects
System.out.println(c.equals(d)); // true
Saying == on Integer always compares values, or not knowing wrappers can be null.
| == | checks whether both variables point to the same object. |
|---|---|
| equals() | checks whether the characters are the same. |
Trap: literals often share one object, so == can seem to work until it doesn't.
“== compares references: it's true only if both variables point to the exact same object in memory. equals, on a String, compares the actual characters. The confusing part is that two identical string literals usually point to the same object because of the string pool, so == happens to return true and the bug hides. But if one string comes from new String, from user input or from joining strings at run time, it's a different object, and == returns false even though the text is the same. So for comparing text I always use equals, or equalsIgnoreCase when case doesn't matter. I also write "yes".equals(input) with the literal first, so a null input just gives false instead of a NullPointerException.”
String a = "java";
String b = new String("java");
System.out.println(a == b); // false: different objects
System.out.println(a.equals(b)); // true: same characters
Saying == compares values for Strings, or that equals and == are the same thing.
| String | immutable, every change makes a new object. |
|---|---|
| StringBuilder | mutable, fast, not synchronized. |
| StringBuffer | mutable and synchronized, rarely needed today. |
“A String can't change once it's created, so every time I write s = s + something inside a loop, Java builds a brand new String and copies all the old characters into it. In a loop of thousands of iterations that's a lot of copying and garbage. StringBuilder holds a growable buffer of characters that I change in place with append, so it's the one I use to build a long string. StringBuffer does the same job but its methods are synchronized, which makes it safe to share between threads but a bit slower. In practice I almost never share a string being built across threads, so I pick StringBuilder, and I call toString once at the end.”
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append(i).append(',');
}
String result = sb.toString(); // "1,2,3,4,5,"
Saying StringBuilder is thread-safe, or not knowing that String is immutable.
Static variable: one copy shared by every object of the class.
Static method: called on the class, can't use this or instance fields directly.
Static block: runs once when the class is initialised.
“static means the thing belongs to the class, not to any one object. A static variable has a single copy that all objects share, so if I keep a counter of how many Student objects were created, I make it static and every constructor increments the same number. A static method is called on the class name, like Math.max, and because there's no object involved it can't use this or touch instance fields directly. A static block is code that runs once, when the class is first initialised, before any object is created, so it's a place to set up static data like a lookup table. The common mistake is making things static just to call them from main, which ends up turning everything into global state.”
class Student {
static int count = 0; // shared by all students
String name; // one per student
Student(String name) {
this.name = name;
count++;
}
}
Saying static means the value can't be changed, which is what final does.
| final | a keyword for variables, methods and classes that can't be changed, overridden or extended. |
|---|---|
| finally | a block after try that runs whether or not an exception happened. |
| finalize | an old cleanup method, deprecated, not to be relied on. |
“They sound alike but do completely different jobs. final is a keyword: a final variable can be assigned only once, a final method can't be overridden, and a final class, like String, can't be extended. finally is a block that goes with try and catch; it runs whether the try finishes normally or throws, so it's where I used to close files or database connections. finalize is a method on Object that the garbage collector might call before reclaiming an object. It was never guaranteed to run at a predictable time, and it's now deprecated, so I wouldn't use it. For cleanup I use try-with-resources instead. One detail people miss: a final reference to a list stops me reassigning the variable, but I can still add items to the list.”
Saying finalize is the right place to release resources, or that final makes an object immutable.
Approach: two pointers, one from each end, moving inward.
Stop early: return false on the first mismatch.
Edges and cost: empty string, case, spaces; O(n) time, O(1) extra space.
“I'd use two pointers. One starts at the first character and one at the last. While left is less than right, I compare the two characters; if they differ it's not a palindrome and I return false straight away. If they match, I move left forward and right back. If the pointers meet or cross without a mismatch, it's a palindrome. That's O(n) time and no extra space, because I never build a reversed copy. Before coding I'd ask whether case and spaces count. If the interviewer says 'Madam' should pass, I lower-case the string first, and for phrases I'd skip anything that isn't a letter or digit. An empty string or a single character counts as a palindrome and falls out of the loop naturally.”
static boolean isPalindrome(String s) {
String t = s.toLowerCase();
int left = 0, right = t.length() - 1;
while (left < right) {
if (t.charAt(left) != t.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
Building a reversed string with += in a loop and not noticing the cost, or never testing an empty string.
Brute force: two nested loops, O(n squared), mention it and move on.
Better: a HashSet of seen values; add returns false for a repeat.
Clean output: a second set so each duplicate prints once.
“The first idea is two nested loops comparing every pair, which works but is O(n squared). A better way is to walk the array once with a HashSet called seen. HashSet's add method returns false if the value is already there, so the moment add fails I know it's a repeat. To avoid printing a value three times when it appears three times, I keep a second set of duplicates and print that at the end. Both sets give average constant-time lookups, so the whole thing is O(n) time and O(n) extra space. If the interviewer says I can't use extra memory, I'd sort the array first and then compare neighbours, which is O(n log n) time.”
static Set<Integer> findDuplicates(int[] nums) {
Set<Integer> seen = new HashSet<>();
Set<Integer> dups = new LinkedHashSet<>();
for (int n : nums) {
if (!seen.add(n)) {
dups.add(n);
}
}
return dups;
}
Stopping at the nested-loop answer, or printing the same duplicate several times without noticing.
Recursive: base cases for 0 and 1, then fib(n-1) plus fib(n-2).
Loop: keep only the last two numbers.
Cost: recursion repeats work and grows exponentially; the loop is O(n).
“The recursive version reads just like the definition: if n is 0 or 1, return n, otherwise return fib of n minus 1 plus fib of n minus 2. It's short, but it recomputes the same values again and again, so the number of calls grows exponentially, and for large n it can also overflow the stack. The loop version keeps two variables, the previous two numbers, and slides them forward n times, so it's O(n) time and constant space. That's the one I'd use. If the interviewer wants recursion anyway, I'd add memoization, storing each result in an array so each value is computed once. I'd also mention that int overflows fairly early, so for bigger n I'd switch to long or BigInteger.”
static int fibRecursive(int n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
static long fibLoop(int n) {
long prev = 0, curr = 1;
if (n == 0) return 0;
for (int i = 2; i <= n; i++) {
long next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
A recursive version with no base case, or claiming the naive recursion is as fast as the loop.
Track two: largest and second, both starting at the smallest possible value.
Update rule: a new largest pushes the old one down to second.
Edges: repeated maximums, fewer than two distinct values.
“I keep two trackers, largest and second. In the code they're long values starting at Long.MIN_VALUE, so even Integer.MIN_VALUE in the array counts as a real number. For each number, if it's bigger than largest, the old largest drops to second and the number becomes largest. Otherwise, if it's bigger than second and not equal to largest, it becomes the new second. I'd first ask whether they mean the second largest distinct value; usually they do, so in 9, 9, 4 the answer is 4, and the not-equal check handles that. It's one pass, O(n) time and constant space, which beats sorting at O(n log n). If second was never updated, there's no answer, for example with one element or all values equal, so I return an empty OptionalInt instead of a misleading number.”
static OptionalInt secondLargest(int[] nums) {
long largest = Long.MIN_VALUE, second = Long.MIN_VALUE;
for (int n : nums) {
if (n > largest) {
second = largest;
largest = n;
} else if (n > second && n != largest) {
second = n;
}
}
return second == Long.MIN_VALUE ? OptionalInt.empty() : OptionalInt.of((int) second);
}
Sorting and taking the second-last element, which fails when the largest value repeats.
List: ordered, allows duplicates, access by position.
Set: no duplicates, fast contains.
Map: key to value, look up by key.
Project example: one use of each, briefly.
“I pick based on what the data needs. A List keeps things in order and allows duplicates, so in my quiz app the questions for a test were an ArrayList, because order mattered and I read them by index. A Set doesn't allow duplicates and answers contains quickly, so I used a HashSet of question ids a student had already attempted, to avoid showing one twice. A Map stores key-value pairs, so scores were a HashMap from student id to their total, which let me update one student's score directly instead of searching a list. If I needed the keys sorted, say for a leaderboard by name, I'd switch to a TreeMap, and LinkedHashMap if I wanted insertion order.”
Using a List and calling contains inside a loop where a Set was the obvious fit.
| Comparable | the class defines its one natural order with compareTo. |
|---|---|
| Comparator | a separate object that defines any order you want. |
Code: Comparator.comparing, with thenComparing and reversed.
“Comparable is implemented by the class itself: Student implements Comparable and writes compareTo, which gives it one natural order, say by roll number. Then Collections.sort on a list of students uses that order. Comparator is a separate object that describes an order from the outside, so I can have as many as I like, by marks, by name, by marks then name, without touching the Student class. That also works for classes I can't edit. In modern Java I build comparators with Comparator.comparing, so sorting by marks highest first, then by name, is one readable line. When writing compareTo by hand I use Integer.compare rather than subtracting two numbers, because subtraction can overflow and give the wrong sign.”
record Student(String name, int marks) {}
List<Student> list = new ArrayList<>(students);
list.sort(Comparator.comparingInt(Student::marks)
.reversed()
.thenComparing(Student::name));
Mixing up which one uses compareTo and which uses compare, or not knowing you can have many Comparators.
| throw | a statement that raises one exception object right now. |
|---|---|
| throws | part of a method signature, warning callers what may come out. |
Uncaught: it climbs the call stack; if it reaches the top, the thread ends with a stack trace.
“throw is an action: inside a method I write throw new IllegalArgumentException with a message, and that exception starts travelling immediately. throws is a declaration in the method signature, like throws IOException, telling callers this method might pass that exception up to them, and for checked exceptions the compiler makes them handle it or declare it too. When an exception is thrown and not caught, Java looks up the call stack: the current method ends, then its caller, and so on, until some method has a matching catch. If none does and it reaches the top of the thread, in a simple program that's main, the thread ends and the stack trace is printed. In a console program that means the program crashes with that trace.”
static int parseAge(String text) throws NumberFormatException {
int age = Integer.parseInt(text);
if (age < 0) {
throw new IllegalArgumentException("Age can't be negative: " + age);
}
return age;
}
Treating throw and throws as the same keyword, or thinking an uncaught exception is silently ignored.
Cause: calling a method or reading a field through a reference that is null.
Reading it: the stack trace line, and the helpful message newer Java versions print.
Habits: initialise fields, return empty collections, check inputs early.
“A NullPointerException happens when I use a reference that points to nothing: calling a method on it, reading a field, taking the length of a null array, or unboxing a null Integer. When I hit one, I start with the line in the stack trace, and newer Java versions even say which variable was null. To avoid them, I initialise fields when I declare them, for example a list starts as an empty ArrayList instead of null. Methods that return collections return an empty list rather than null, so callers can loop safely. I check parameters at the start of a method with Objects.requireNonNull, so the error appears where the bad value came in, not three calls later. And for string comparisons I put the literal first.”
Saying the fix is to wrap code in try-catch for NullPointerException.
Two basic ways: extend Thread, or pass a Runnable (often a lambda) to a Thread.
Prefer Runnable: keeps your class free to extend something else and separates the task from the thread.
start vs run: start makes a new thread; run just calls a method on the current one.
“The two basic ways are extending Thread and overriding run, or writing a Runnable and passing it to a new Thread. I prefer Runnable, usually as a lambda, because Java only allows one parent class, and it keeps the task separate from the thread that runs it. In real code I'd hand tasks to an executor rather than making threads by hand. The start versus run difference is a classic trap. start asks the JVM to create a new thread, and that new thread calls run. If I call run directly, no new thread is created; the code just runs on the current thread like any normal method call. Also, a thread can be started only once. Calling start a second time throws IllegalThreadStateException.”
Runnable task = () ->
System.out.println("Running on " + Thread.currentThread().getName());
Thread t = new Thread(task);
t.start(); // runs on a new thread
task.run(); // runs on the current thread
Saying run() and start() do the same thing, or not knowing that calling run() creates no new thread.
The project: what it did and who used it, in two lines.
Your part: the classes or features you wrote, and one decision you made.
Looking back: one concrete thing you'd change and why.
“In my final year, three of us built an attendance system for our department in Java with a MySQL database. I wrote the part that marked attendance and produced the monthly report. I designed a small Student class and an AttendanceRecord class, and used a HashMap from student id to their records so the report didn't query the database once per student. The trickiest bug was duplicate entries when a teacher clicked submit twice, which I fixed with a unique constraint and a check before inserting. Looking back, I put SQL strings directly into the screen classes, which made testing hard. Now I'd move all database code into one data-access class and write unit tests for the report logic.”
Describing only what the team did, never 'I', or being unable to explain a class you claim to have written.
The symptom: what went wrong and why it was confusing.
The method: stack trace, debugger, breakpoints, or cutting the problem down.
The lesson: the cause, the fix, and a habit you kept.
“In my internship I built a small dashboard that showed what share of support tickets each team closed on time. Almost every team showed zero, and one showed exactly a hundred. I assumed the database query was wrong and lost most of a day there. Then I stopped guessing, printed the raw counts, and they were correct. So I put a breakpoint on the line that did the maths: closedOnTime divided by total, times 100, all ints. Integer division threw away the fraction, so anything below one became zero before the multiply. I changed it to closedOnTime times 100.0 divided by total, and added a guard for teams with no tickets. Since then, whenever a number looks wrong, I check the types in the calculation first, and I write a small test with numbers I've worked out by hand.”
A story that ends with 'I rewrote it and it worked' with no idea what the cause was.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.