The interviewer says, "Write a program to find the second largest number in an array." You have done this one. You write two loops, sort the array, return the second-last element. They nod and ask, "What if the largest number appears twice?" Then, "Can you do it without sorting?" Then, "What is the complexity now?" The program was never the test. The three questions after it were.
That is how Java coding rounds for freshers work. The programs come from a small, well-known pool. What separates candidates is whether they can explain the approach, name the cost and handle the edge case before the interviewer points at it.
The short answer
Java coding interview questions for freshers cluster into six groups: string programs, number programs, array programs, collection programs, Java 8 stream programs and a couple of classic design programs like a thread-safe singleton. For each of the 25 below you get why interviewers ask it, the approach in plain words with the Java API that does the work, the time and space complexity, the edge cases that break naive answers, and the follow-up you are most likely to hear next. There is no code to copy here on purpose: if you can explain a solution in sentences, you can write it, and in the room the explaining is what gets scored.
This post covers coding programs. For the conceptual side of Java, such as the JVM, equals and hashCode, and how a HashMap works inside, read our Java interview questions for freshers first.
How to answer any program in the room
Before the list, a routine that works for every question in it. Restate the problem and ask one clarifying question (empty input? duplicates? case-sensitive?). Describe the brute force and its complexity in one sentence. Give the better approach and say why it is better. Write it. Then walk through one normal input and one edge case out loud. Our coding interview preparation guide for freshers explains why this out-loud half is the part most preparation skips.
String programs
1. Reverse a string
Interviewers ask this to see whether you know that a String is immutable and what that costs. The library answer is new StringBuilder(s).reverse().toString(). The manual answer is to call toCharArray(), swap characters from both ends with two pointers moving inward until they meet, and build a new String from the array.
Complexity: O(n) time, O(n) space, because you cannot reverse a String in place and must create a new one.
Edge cases: null (decide whether to return null or throw), the empty string, and a single character. One subtle point: StringBuilder.reverse() keeps surrogate pairs (characters such as emoji that take two char values) in the right order, while a naive char swap splits them.
Likely follow-up: "Do it recursively." You can, but building the result with substring and concatenation copies the string at every level, so it is O(n²) time and uses O(n) stack depth, which can throw StackOverflowError on a long string. Say that and prefer the iterative version.
2. Check whether a string is a palindrome
A palindrome reads the same both ways, like "madam". Use two pointers, one at each end, compare charAt(left) and charAt(right), and move them inward; return false at the first mismatch. Do not reverse the string and compare with equals, which works but uses O(n) extra space for no reason.
Complexity: O(n) time, O(1) extra space.
Edge cases: the empty string and a single character are palindromes. Ask whether case and punctuation count. For "A man, a plan, a canal: Panama", skip characters where Character.isLetterOrDigit is false and compare with Character.toLowerCase.
Likely follow-up: "Check if a number is a palindrome without converting it to a string." Reverse the digits with % 10 and / 10 and compare. A cleaner version reverses only the second half of the number, which also avoids overflow on large inputs. Negative numbers are not palindromes.
3. Check whether two strings are anagrams
"listen" and "silent" use the same letters. The simple approach is to convert both to char[], call Arrays.sort on each and compare with Arrays.equals. The better approach, for lowercase English letters, is an int[26] count array: increment for each character of the first string, decrement for the second, and check that every count ends at zero.
Complexity: sorting is O(n log n); the count array is O(n) time and O(1) space, since the array size is fixed.
Edge cases: return false immediately if the lengths differ. Ask about case and spaces ("Dormitory" and "dirty room"). For arbitrary Unicode, use a HashMap<Character, Integer> instead of a fixed array.
Likely follow-up: "Group a list of words into anagram groups." Use the sorted characters of each word as the key in a HashMap<String, List<String>>.
4. Find the first non-repeating character
Asked because it needs two passes and a data structure, and freshers often try to do it in one pass and get it wrong. First pass: count every character. Second pass: walk the string again, in its original order, and return the first character whose count is 1. Counts go in an int[128] for ASCII (int[256] covers Latin-1 too) or a HashMap<Character, Integer> in general.
Complexity: O(n) time, O(k) space where k is the number of distinct characters.
Edge cases: no non-repeating character exists ("aabb"), so decide on a return value before writing: a sentinel like '\0', -1 for an index, or an Optional<Character>. Also ask whether "A" and "a" are the same character.
Likely follow-up: "Can you avoid the second pass over the string?" Use a LinkedHashMap, which keeps insertion order, and in the second pass iterate the map instead of the string. That helps when the string is very long but the alphabet is small.
5. Count the frequency of each character using a HashMap
The standard warm-up for HashMap. Loop over the characters and, for each one, call map.merge(c, 1, Integer::sum), or the older map.put(c, map.getOrDefault(c, 0) + 1). Being able to say what merge does (insert 1 if absent, otherwise combine with the existing value) shows you know the API rather than a memorised pattern.
Complexity: O(n) time on average, O(k) space for k distinct characters.
Edge cases: spaces and punctuation (skip them if asked), case, and the empty string, which should return an empty map rather than null.
Likely follow-up: "Print the result in alphabetical order" (use a TreeMap, or sort the entries) or "Return the most frequent character" (track the maximum while counting, and ask what to do on a tie).
6. Reverse the words in a sentence
"Java is fun" becomes "fun is Java". Call trim(), then split("\\s+") so that runs of spaces count as one separator, and append the words to a StringBuilder from last to first with a single space between them. String.join(" ", ...) over a reversed list also works.
Complexity: O(n) time and O(n) space.
Edge cases: leading, trailing and repeated spaces. Without trim(), a sentence that starts with a space makes split return an empty first element, and your output ends with a stray space. Also handle a single word and an all-space string.
Likely follow-up: "Do it in place on a char[]." Reverse the whole array, then reverse each word inside it. Two passes, O(1) extra space.
Number programs
7. Check whether a number is prime
Asked to see whether you stop at the square root. A number n is prime if it is greater than 1 and no number from 2 up to √n divides it, because any factor larger than √n pairs with one smaller than √n. Handle 2 separately, then test only odd divisors. Write the loop condition as i * i <= n rather than calling Math.sqrt, which avoids floating-point rounding.
Complexity: O(√n) time, O(1) space.
Edge cases: 0, 1 and negative numbers are not prime; 2 is the only even prime. For n close to Integer.MAX_VALUE, i * i can overflow an int, so use long for i or write the condition as i <= n / i.
Likely follow-up: "Print all primes up to N." Use the Sieve of Eratosthenes: a boolean[] where you cross out multiples of each prime starting from its square. O(n log log n) time, O(n) space.
8. Fibonacci: iterative versus recursive
The point of this question is the comparison. The naive recursive version, fib(n - 1) + fib(n - 2), recomputes the same values again and again, so its running time grows exponentially (roughly 1.6 to the power n). The iterative version keeps just the previous two values in two variables and loops n times.
Complexity: recursive is exponential time and O(n) stack depth; iterative is O(n) time and O(1) space. Recursion with memoization (store each result in an array or HashMap) brings recursion down to O(n) time but O(n) space.
Edge cases: n = 0 and n = 1, and negative n (reject it). Overflow arrives sooner than people expect: with fib(0) = 0, fib(46) is the largest that fits in an int and fib(92) the largest that fits in a long. Beyond that, use BigInteger.
Likely follow-up: "Can you do better than O(n)?" Yes, matrix exponentiation computes fib(n) in O(log n) multiplications. Mentioning it is enough at fresher level.
9. Factorial, and what happens on overflow
Anyone can write the loop. The interviewer is waiting to see whether you notice that factorials outgrow Java's types almost immediately. Multiply from 1 to n in a loop; a recursive version is fine to mention but uses O(n) stack.
Complexity: O(n) time, O(1) space for the loop.
Edge cases: 0! is 1. Negative input should throw an IllegalArgumentException. 12! is the largest factorial that fits in an int and 20! the largest that fits in a long; 13! in an int silently wraps around to a wrong number, because Java integer arithmetic does not throw on overflow.
Likely follow-up: "How would you detect the overflow?" Use Math.multiplyExact, which throws an ArithmeticException instead of wrapping, or switch to BigInteger for large n.
10. Check for an Armstrong number
An Armstrong number equals the sum of its digits, each raised to the power of the number of digits: 153 = 1³ + 5³ + 3³. First count the digits (divide by 10 until zero), then extract each digit with % 10 and / 10, raise it to that power and add it up, and compare the sum with the original number. Keep a copy of the original, because the loop destroys the variable you divide.
Complexity: O(d) digit operations, where d is the number of digits.
Edge cases: single-digit numbers are all Armstrong numbers. Ask how to treat negatives. Math.pow returns a double; for small digits it is exact, but a small loop that multiplies integers avoids the type conversion entirely and is easy to justify.
Likely follow-up: "Print all Armstrong numbers between 1 and 1000." The same check in a loop; the three-digit ones are 153, 370, 371 and 407.
11. Swap two numbers without a third variable
Two versions. Arithmetic: a = a + b; b = a - b; a = a - b;. XOR: a = a ^ b; b = a ^ b; a = a ^ b;. Be ready to trace one of them with real numbers.
Complexity: O(1) time and space either way.
Edge cases: the common answer is "the arithmetic version breaks on overflow", and in Java that is not quite right. Java int addition wraps around in two's complement, and the subtraction wraps back, so the swap still produces the correct values. The real trap is aliasing, and it hits both versions: if both operands are the same memory location, such as arr[i] and arr[j] with i == j, the XOR version zeroes it on the first step and the arithmetic version on the second, and the value is lost.
Likely follow-up: "Why can't you write a swap(int a, int b) method in Java?" Because Java passes arguments by value: the method gets copies, and swapping copies changes nothing for the caller. You would swap two elements of an array instead.
Array programs
12. Find duplicate elements in an array
Brute force compares every pair, O(n²). The standard answer is a HashSet: add returns false when the element is already there, so that element is a duplicate. To report each duplicate once even if it appears three times, collect them into a second set.
Complexity: O(n) time on average, O(n) space. Sorting first and comparing neighbours is O(n log n) time with no extra set, which is the answer when memory is tight.
Edge cases: an empty array, no duplicates, and all elements equal. Ask whether you may modify the array, because sorting changes it.
Likely follow-up: "The array holds n + 1 numbers in the range 1 to n, and exactly one value is repeated. Find it in O(1) extra space without modifying the array." Treat each value as a pointer to an index and use Floyd's cycle detection; the duplicate is where the cycle starts.
13. Find the second largest element
The trap in the opening of this post. Sorting gives O(n log n) and still gets duplicates wrong: in [5, 5, 3], the second-last element is 5, but the second largest distinct value is 3. The better answer is one pass that keeps largest and second. When a value beats largest, the old largest moves down to second; when a value is strictly between them, it becomes second.
Complexity: O(n) time, O(1) space.
Edge cases: fewer than two elements, or all elements equal, where no second largest exists; say what you return. Initialising both trackers to Integer.MIN_VALUE gives a wrong answer when the array actually contains Integer.MIN_VALUE, so use a boolean flag, a long, or seed them from the array itself.
Likely follow-up: "Find the kth largest." Keep a PriorityQueue (a min-heap) of size k; whenever it grows past k, poll the smallest. The top of the heap is the answer. O(n log k) time.
14. Find the missing number from 1 to n
The array holds n − 1 distinct numbers from 1 to n. The sum of 1 to n is n(n + 1) / 2, so subtract the array's sum from that and the difference is the missing number. An alternative is to XOR all numbers from 1 to n with all array elements; every present number cancels out and the missing one remains.
Complexity: O(n) time, O(1) space.
Edge cases: for large n, n(n + 1) can overflow an int, so compute in long, or use the XOR version, which cannot overflow. Ask whether the missing number can be n itself, and check your formula still handles it.
Likely follow-up: "Two numbers are missing." Use both the sum and the sum of squares (two equations, two unknowns), or the XOR approach split by one set bit.
15. Two-sum using a HashMap
Given an array and a target, return the indices of two numbers that add up to the target. Probably the most-asked array question, because it is the cleanest demonstration of trading space for time. Brute force tries every pair, O(n²). The better version walks the array once with a HashMap<Integer, Integer> from value to index: for each element, compute target - nums[i]; if that complement is already a key, you have the pair; otherwise put the current value and index.
Complexity: O(n) time on average, O(n) space.
Edge cases: check for the complement before you put the current element, or an element can pair with itself (target 10, element 5 appearing once). The same order also handles duplicate values, such as [3, 3] with target 6. Decide what to return when no pair exists.
Likely follow-up: "The array is sorted. Can you use O(1) space?" Two pointers, one at each end: if the sum is too small, move the left one right; if too big, move the right one left.
16. Rotate an array by k positions
Rotating [1, 2, 3, 4, 5] right by 2 gives [4, 5, 1, 2, 3]. The easy version copies each element to index (i + k) % n in a new array. The interview answer is the reversal algorithm, done in place: reverse the whole array, then reverse the first k elements, then reverse the remaining n − k.
Complexity: the extra-array version is O(n) time and O(n) space; the reversal version is O(n) time and O(1) space.
Edge cases: k larger than n, so set k = k % n first; k = 0; and an empty array, where k % n throws an ArithmeticException because n is zero, so return early.
Likely follow-up: "Rotate left instead." Rotating left by k is rotating right by n − k. For a List, the library already has Collections.rotate(list, k).
17. Merge two sorted arrays
Keep one pointer in each array and write the smaller of the two current elements into a result array of size m + n, advancing that pointer. When one array runs out, copy the rest of the other. This is the merge step of merge sort, which is why interviewers like it.
Complexity: O(m + n) time, O(m + n) space for the result.
Edge cases: one or both arrays empty, and equal elements; taking from the first array on a tie keeps the merge stable.
Likely follow-up: "The first array has enough empty space at the end to hold the second. Merge in place." Fill from the back: compare the largest remaining elements of each and write the bigger one into the last free slot. Working backwards means you never overwrite an element you have not used yet.
Collection programs
These three test whether you know what the collections framework already gives you. If the classes behind them feel unfamiliar, read the data structures you must know first.
18. Remove duplicates from a list
The interviewer wants to hear about order. new ArrayList<>(new HashSet<>(list)) removes duplicates but does not preserve the order. new ArrayList<>(new LinkedHashSet<>(list)) removes them and keeps the first occurrence of each in its original position. In Java 8 style, list.stream().distinct().collect(Collectors.toList()) also keeps encounter order.
Complexity: O(n) time on average, O(n) space.
Edge cases: all of these rely on the elements' equals and hashCode. For a list of your own objects that does not override them, only references to the very same object count as duplicates; two equal-looking objects are both kept. null elements are allowed in a HashSet and in distinct().
Likely follow-up: "Remove them from the original list while looping over it." Calling list.remove inside a for-each loop usually throws a ConcurrentModificationException (and when it does not, it silently skips an element). Use an Iterator and its remove() method, or removeIf with a set that tracks what you have seen.
19. Sort a HashMap by value
A HashMap has no order, and a TreeMap sorts by key, so neither does this on its own. Stream the entries, sort them with Map.Entry.comparingByValue() (pass it Comparator.reverseOrder() for descending), and collect into a LinkedHashMap so the sorted order survives. The four-argument Collectors.toMap, whose last argument is LinkedHashMap::new, does this in one step. Before Java 8, you copied entrySet() into a list, called Collections.sort with a comparator, and inserted the entries into a LinkedHashMap.
Complexity: O(n log n) time, O(n) space.
Edge cases: collecting into a plain HashMap at the end throws the sorting away, which is the most common bug. Equal values: add .thenComparing(Map.Entry.comparingByKey()) if the output must be deterministic; chained like that, Java often cannot infer the types, so write Map.Entry.<String, Integer>comparingByValue() for the first comparator.
Likely follow-up: "Why not use a TreeMap with a comparator on the values?" A TreeMap treats two keys as the same key when its comparator returns 0, so keys with equal values overwrite each other, and lookups break because the comparator does not match the map's keys.
20. Implement an LRU cache with LinkedHashMap
A least-recently-used cache of fixed capacity evicts the entry that has gone unused the longest. Extend LinkedHashMap and call its three-argument constructor with accessOrder set to true, so every get or put moves that entry to the end of the internal order. Then override removeEldestEntry to return true when size() exceeds the capacity; the map calls it after each insertion and removes the eldest (least recently used) entry for you.
Complexity: get and put are O(1) on average. Space is O(capacity).
Edge cases: capacity of zero or less (reject it), and updating an existing key, which should refresh its position without evicting anything. LinkedHashMap is not thread-safe, and in access-order mode even get changes the internal order, so concurrent reads need a lock too. Collections.synchronizedMap is the simple wrapper.
Likely follow-up: "Now build it without LinkedHashMap." Use a HashMap from key to node, plus your own doubly linked list with dummy head and tail nodes. On access, unlink the node and move it to the front; on overflow, remove the node before the tail and delete its key from the map. This is a standard machine-coding and low-level design question, so be ready to draw it.
Java 8 stream programs
Java 8 coding interview questions are mostly the same programs, written with streams. Know Collectors, know that max and min return an Optional, and be ready to say when a plain loop is clearer.
21. Group by, and find duplicates with streams
Grouping is the most-asked stream question. Collectors.groupingBy(Employee::getDepartment) returns a Map<String, List<Employee>>. Add a downstream collector for summaries: groupingBy(Employee::getDepartment, Collectors.counting()) gives the head count per department as Map<String, Long>, and Collectors.averagingDouble(Employee::getSalary) gives the average salary. Grouping strings by length with groupingBy(String::length) is the fresher version.
The same tool finds duplicates: groupingBy(Function.identity(), Collectors.counting()) gives each element's count, then stream that map's entries, keep the ones whose value is greater than 1, and map to the key. Some candidates write filter(x -> !seen.add(x)) with an outside HashSet instead; it works on a sequential stream, but the lambda changes outside state, which streams are designed to avoid and which breaks on a parallel stream. Say that if you use it.
Complexity: O(n) time on average for both, O(n) space for the map.
Edge cases: groupingBy makes no guarantee about the map type or its order (in practice it is a HashMap); pass TreeMap::new as the map factory if the output must be sorted. A null classifier result throws a NullPointerException.
Likely follow-up: "What is partitioningBy?" It is a groupingBy with a true/false predicate, always returning a map with exactly two keys, true and false, even if one list is empty.
22. Find the maximum and minimum with streams
On a Stream<Integer>, max(Comparator.naturalOrder()) returns an Optional<Integer>. On an IntStream (via mapToInt(Integer::intValue)), max() returns an OptionalInt. For objects, pass a comparator: employees.stream().max(Comparator.comparingDouble(Employee::getSalary)) finds the highest-paid employee. If you need both ends in one pass, summaryStatistics() on an IntStream gives min, max, count, sum and average together.
Complexity: O(n) time, O(1) space.
Edge cases: an empty stream returns an empty Optional, so calling get() without checking throws a NoSuchElementException. Use orElse, orElseThrow or ifPresent, and say why.
Likely follow-up: "Find the second highest salary with streams." Map to salaries, call distinct(), sort with Comparator.reverseOrder(), then skip(1).findFirst(). Without distinct(), two people sharing the top salary give the wrong answer. Mention that this sorts, so it is O(n log n), while a loop like question 13 is O(n).
23. Count the words in a sentence with streams
Lowercase the text, split it on non-word characters with split("\\W+"), turn the array into a stream with Arrays.stream, drop empty strings with filter, and collect with groupingBy(Function.identity(), Collectors.counting()). The result maps each word to how often it appears.
Complexity: O(n) time in the length of the text, O(w) space for w distinct words.
Edge cases: leading punctuation produces an empty first element, which is why the filter is there. \\W treats apostrophes and non-English letters as separators, so "don't" becomes "don" and "t"; mention this and ask whether it matters. An empty input should give an empty map.
Likely follow-up: "Return the three most frequent words." Stream the map's entries, sort by Map.Entry.comparingByValue(Comparator.reverseOrder()), limit(3), and collect. For counting characters instead of words, start from s.chars(), which gives an IntStream, and use mapToObj(c -> (char) c) before grouping.
Classic OOP and design programs
These two check whether you understand objects, not just syntax. The theory behind them is in our OOPs interview questions.
24. Write a thread-safe singleton
A singleton class allows exactly one instance. Start with a private constructor and a static method that returns the instance. Then explain the thread-safe options, because the lazy version with a plain if (instance == null) check can create two instances when two threads reach it together.
- Eager initialisation: a
private static finalfield created when the class loads. Simple and thread-safe, but not lazy. - Double-checked locking: check for
null, enter asynchronizedblock, check again, then create. The field must bevolatile. Without it, the write of the reference can be reordered with the constructor's writes, so another thread can see a non-null reference to an object that is not fully built. - Initialisation-on-demand holder: a private static nested class holds the instance. The JVM initialises that class only when it is first used and guarantees class initialisation is thread-safe, so you get laziness with no locking code.
- Enum singleton: an
enumwith one constant. Joshua Bloch's *Effective Java* calls it often the best way to implement a singleton, because the JVM guarantees one instance even against serialization and reflection.
Complexity: O(1) to get the instance. After initialisation, only the double-checked version pays even a volatile read on each call.
Likely follow-up: "How can a singleton be broken?" Reflection can call the private constructor (guard by throwing if an instance already exists), deserialization creates a new object (implement readResolve to return the existing one), and clone() can copy it if the class is cloneable. The enum version resists all three.
25. Create an immutable class
Interviewers ask this because String is immutable and they want to know whether you can explain how. The rules: make the class final so no subclass can add mutable behaviour; make every field private final; provide no setters; set everything in the constructor; and make defensive copies of any mutable fields, both when they come in and when a getter hands them out.
That last rule is the one candidates miss. If the constructor stores the caller's List or Date directly, the caller can still change it from outside. Copy it on the way in (List.copyOf since Java 10, or new Date(d.getTime())), and either return an unmodifiable view or a copy from the getter.
Edge cases: a field holding a mutable object that you do not copy makes the whole class mutable, even with final. final stops the reference from changing, not the object it points to.
Likely follow-up: "What are records?" Since Java 16, a record gives you a final class with private final fields, a constructor, accessors, equals, hashCode and toString in one line. Records are only shallowly immutable: a record holding a List still needs a defensive copy in its compact constructor.
How to practise these programs
Do not practise by reading. Pick a program, close this page, and explain it out loud in under two minutes: the approach, the Java API you would use, the complexity, and two edge cases. Then write it from scratch and test it on the edge cases you just named. If you cannot explain it without looking, you do not know it yet, however many times you have typed it.
Practise the follow-ups too. They are where marks are won, and they are predictable: "can you do it without extra space", "what if there are duplicates", "what if the input is empty", "what is the complexity now". When a follow-up does catch you out, there is a better move than going quiet: see how to handle a question you don't know.
To practise the explaining half under a clock, pick Java on our Java interview practice page for a round with Sia, the AI interviewer. Sia does not give you a code editor; you speak or type your answer against a timer, and it asks for exactly what this post trains: the approach, the steps, the complexity and the edge cases. Every answer is scored, and you get a written write-up at the end. When you want a person instead, a live Zoom interview with a working engineer is rated on technical depth, problem solving, communication, behaviour and code quality, with written feedback afterwards.