Loading...
Loading...
Java interview guide
Lambdas, streams, Optional, records and the newer language features. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
59 questionsLast updated
Optional.or for, and how is it different from orElseGet?or, added in Java 9, returns this Optional if present, or else the Optional produced by a supplier. It is for fallbacks like cache then database. orElseGet returns a plain value and ends the chain, while or keeps an Optional so you can keep chaining.
reduce(0, Integer::sum) return an int while reduce(Integer::sum) returns an Optional?The first form has an identity value, 0, which is also the correct result for an empty stream, so there is always an answer. The second form has no starting value, so for an empty stream there is nothing to return, and it uses an Optional.
Lesson: Optional results from streamsBecause -1 can be a real value, for example a temperature or a balance change, and callers can forget to check for it and print it as a result. An Optional cannot be mistaken for a real value; the caller must decide what the empty case means.
Lesson: Optional results from streamsEvery caller has to wrap their argument, which adds noise, and nothing stops a caller from passing null, so the method now has three cases to handle. Overloaded methods, one with the argument and one without, express an optional input more clearly.
Optional<T> return null? What happens?It compiles, because Optional is a normal class, but it breaks the contract. Callers write result.orElse(...) or result.map(...) and get a NullPointerException on the Optional itself. Such methods must always return Optional.empty() for no value.
findInCache(id).orElse(loadFromDb(id)). Why?The argument of orElse is evaluated before the call, so loadFromDb now runs on every request, even on cache hits. The answers are still correct, which is why tests did not catch it. orElseGet(() -> loadFromDb(id)) restores the old behaviour.
null, and you cannot change them. Can you still use an Optional chain?Yes. Start with Optional.ofNullable(root) and use map for each getter: map wraps the result with ofNullable, so a null at any step becomes an empty Optional. flatMap is only needed when the getters themselves return Optionals.
LocalDate have no public constructor?It uses factory methods like of, parse and now instead. They have clear names, check that the date is valid, and leave the class free to reuse or cache objects. A wrong date throws DateTimeException right away.
I call isLeapYear() on a LocalDate, or Year.isLeap(year). The rule is: divisible by 4, but century years must also be divisible by 400, so 1900 is not a leap year and 2000 is.
LocalTime a bad choice for an overnight train's arrival?LocalTime has no date, so adding hours past midnight wraps round and the day change is lost. LocalDateTime moves to the next date, so the arrival shows the right day.
For the stored value I would use truncatedTo(ChronoUnit.MINUTES), which drops the seconds and nanoseconds. For display only, a formatter with the pattern HH:mm also works without changing the value.
It is 29 February in a leap year and 28 February otherwise. When the day does not exist in the new month, plusMonths uses the last valid day of that month instead of spilling into March.
I write !d.isBefore(start) && !d.isAfter(end). Using isAfter(start) && isBefore(end) would leave out both end days, which is a common off-by-one bug in booking and admission windows.
Period.between(a, b).getDays() give the wrong number of days between two dates?Because it returns only the days part of the period. For 10 January to 15 March the period is 2 months and 5 days, so it returns 5. The total is ChronoUnit.DAYS.between(a, b), which is 65.
toMinutes() and toMinutesPart() on a Duration?toMinutes() is the whole duration in minutes, such as 195. toMinutesPart() is only the minutes left after the full hours, such as 15. The ...Part() methods help build a display like "3h 15m".
An offset is a fixed difference from UTC, like +05:30 or -04:00. A zone such as America/New_York is a set of rules that says which offset applies on each date, including DST changes. One zone can have different offsets during the year.
That local time does not exist. ZonedDateTime moves it forward by the length of the gap, so it becomes 03:30 with the new offset. In the autumn overlap, when a time happens twice, Java picks the earlier offset unless you ask for the later one.
YYYY dangerous in a date pattern?YYYY is the week-based year, not the calendar year. For a few days around New Year it gives the next or previous year, for example 31 December 2024 prints as 2025. The bug passes all tests that do not use those days, so I always use yyyy or uuuu.
For anything a program reads: APIs, JSON, CSV files and logs. ISO text such as 2024-03-15T20:00:00+05:30 is unambiguous and every language can parse it. Custom patterns are for what people read on screen or on paper.
SimpleDateFormat not thread-safe?It stores a Calendar inside and changes it during every format and parse call. When two threads use the same object, they overwrite each other's intermediate values and get wrong results or exceptions. DateTimeFormatter is immutable, so one shared instance is safe.
Date fields. Do you rewrite everything to java.time?Not in one go. I convert at the boundaries, where old APIs or the database hand over values, and write all new logic with java.time. Then I replace old types step by step, with tests around each change.
I loop from the start date to the end date with plusDays(1), both ends included, and count dates that are not Saturday, Sunday or in a holiday set. With streams it is start.datesUntil(end.plusDays(1)).filter(this::isWorkingDay).count().
I move forward one day at a time and decrease N only when the new day is a working day, skipping weekends and holidays. When N reaches 0, that date is the answer. Holidays come from a Set<LocalDate> so the check is fast.
distinct() decide that two elements are the same?It uses hashCode() and equals(), like a HashSet. A record generates both from all its components, so two records with equal values count as one. A plain class that does not override them compares by address, so distinct() keeps every object.
As early as possible, right after skipping the header and splitting. Then parsing and trimming happen in one place, and every later step works with s.branch() instead of p[2]. If a format changes, only that one map needs editing.
A factory has a name that says what the input is, like ofRupees versus ofPaise, or fromCsv for a line of text. It can also hide parsing, return a cached or shared value, or return Optional for input that may be bad. It still calls the constructor, so all validation still runs.
I would give the record a factory like static Optional<Student> tryParse(String line) that returns Optional.empty() when the line has the wrong shape or the constructor throws. Then lines.stream().map(Student::tryParse).flatMap(Optional::stream).toList() keeps the good records, and the size difference tells me how many lines were rejected, which I can log.
It is a record declared inside a method. I use it when a stream needs to carry two or three values together for a few steps, such as a name with a percentage, and the type is not needed anywhere else. It is clearer than Map.Entry or an Object[] and does not add a new top-level type.
They describe the data shape in one line, nested JSON maps to nested records, and they are immutable, so a request cannot change while it is being handled. The compact constructor can reject bad input early, and JSON libraries like Jackson support records directly.
Lesson: Nested and local recordsWhen the business has a fixed set of kinds, like savings and current accounts, an open class lets anyone add a kind that existing code does not handle, and final allows no kinds at all. Sealing records the exact list in the type, so readers and the compiler both know every possible subtype.
No. An anonymous class has no name that could appear in permits, so javac rejects it, and a lambda can only implement a functional interface that is not sealed. You must create one of the permitted classes.
A record is implicitly final, so it meets the rule for permitted subclasses without any extra modifier. It is also a compact, immutable data carrier, so a sealed interface with record children describes a closed family of data shapes in very few lines. The only limit is that records can implement sealed interfaces but not extend sealed classes.
No. javac reports sealed class must have subclasses. If a class should have no children, it should be final instead.
permits clause?When the sealed type and all its children are in the same source file; the compiler then uses the children it finds there. Nesting the records inside the interface is a tidy way to do it. For bigger families split across files, the list must be written out.
Lesson: Sealed interfaces with recordsYes. An enum is implicitly final (or sealed, if its constants have bodies), so it can implement a sealed interface without extra modifiers. It suits kinds with no data, while records suit kinds that carry data.
Lesson: Sealed interfaces with records-1 or null?The type does not say the value can mean failure, so a caller can forget to check and use -1 in a calculation or get a NullPointerException later. They also carry no reason. A Result makes the possibility of failure part of the type and stores why it failed.
As a sealed interface with one record per state, each holding only the data that state has: Paid has a transaction id, Shipped has a tracking id, Cancelled has a reason. Transitions are methods that return a Result with the new state, or a Failure for a move that is not allowed, such as shipping a cancelled order.
else return 0; at the end of an instanceof chain over a sealed type a bad idea?It gives every kind not listed above it the same answer, including kinds added later, and nothing reports it. I end such chains with throw new AssertionError(...), put the behaviour in an interface method, or in Java 21 use an exhaustive switch, so a missing case shows up at compile time or at least loudly in tests.
In the compact constructor. Every factory ends by calling the constructor, so the rules run no matter how the record is built. Factories should only prepare the values, such as converting rupees to paise or parsing a CSV line.
Lesson: Records and sealed types: practiceThe old style repeats the type in the check and in the cast, so the two can drift apart after copy-paste and fail with a ClassCastException at run time. With a pattern, the cast always matches the check, and the code is shorter.
equals with a pattern, and is getClass() ever better?I would write return o instanceof Point p && x == p.x && y == p.y;, which also handles null. instanceof lets a subclass object be equal to a parent object, so for classes meant to be extended some teams use getClass() == o.getClass() instead. For final classes and records, instanceof is fine.
A case dominates a later one when it matches everything the later one would, such as case Number n before case Integer i. The later case can never run, so the compiler rejects it. The fix is to order cases from the most specific type to the most general.
The first matching case from the top wins, just like an if-else chain. Because of that rule, a broader type above a narrower one would hide it, which is why the compiler reports dominance errors.
The unguarded case matches every value of that type. If it came first, the guarded case could never run, so the compiler reports it as dominated. The general rule is specific before general.
Lesson: Guards with whenThe order of the guarded cases for the same type. The compiler does not compare guards, so a loose guard like m >= 35 above a strict one like m >= 90 silently catches the high marks. I would order range guards from strictest to loosest and add a test for each boundary value.
default when switching over a sealed type?Because default silences the compiler. When someone adds a new subtype, a switch with default still compiles and sends the new type down a generic path, which can be wrong. Without default, every incomplete switch becomes a compile error that points at exactly what needs updating.
When the behaviour clearly belongs to each type and new types are added more often than new operations, a method like area() on each class keeps things together. A sealed switch is better when the set of types is stable and new operations, such as reports or fee rules, are added often.
A record guarantees that its state is exactly its components, with matching accessors, so taking it apart is safe and predictable. An ordinary class can hide state and has no standard way to be deconstructed. The record's component list is what tells the pattern how many parts to expect and in what order.
Lesson: Record patterns and deconstructionI would model it as sealed interface Expr permits Num, Add, Mul, Neg with records, then write eval as a switch: case Num(int v) -> v, case Add(Expr l, Expr r) -> eval(l) + eval(r) and so on. Each case recurses into its children until it reaches a number. Adding a new node type then produces compile errors in every switch that must handle it.
var be used for fields or method parameters?Fields, parameters and return types are part of a class's public shape, which other code depends on, so their types should be written explicitly. A local variable's type is visible right next to its value. Keeping var local keeps inference simple and APIs readable.
var in code review?When the right side does not show the type, like var result = service.process();, or when the exact numeric type matters, like var discount = 10; that later needs decimals. I keep var for new expressions, obvious literals, and loop variables over entries, where it removes noise without hiding anything.
Only if the closing """ is on its own line. If you put it right after the last character of the content, there is no final newline. This matters when you compare strings or print with println.
formatted?Only for the fixed shape of the query. Pasting user input into the SQL text allows SQL injection, where the input changes the query. I keep ? placeholders in the text block and pass the values through a PreparedStatement.
list.reversed() a copy?No, it is a view of the same list. Changes to the list show in the view, and changes through the view, if the list is modifiable, change the list. If I need an independent reversed copy I write new ArrayList<>(list.reversed()).
HashSet and HashMap not sequenced?They have no defined order: the iteration order depends on hash codes and can change as the table grows. There is no meaningful first or last element, so they do not implement the sequenced interfaces. If I need order, I use LinkedHashSet or LinkedHashMap.
strip() added when trim() already existed?trim() removes characters up to the plain space (code 32) and misses other Unicode whitespace, such as the em space. strip() uses Character.isWhitespace, so it handles text pasted from websites and phones correctly. New code should prefer strip().
Files.readString and when not?I use it for small files such as configs, templates or test data, because it reads everything in one call. For large files, like big logs or CSV exports, I use Files.lines or a BufferedReader so the whole file is not loaded into memory at once.
instanceof if-else chain?If the types form a closed family, I make the parent sealed and turn the chain into a switch with one case per subtype and when guards for extra conditions, with no default. The compiler then checks completeness and points at every switch when a subtype is added. If the family is open, I keep a default that fails loudly.
var can introduce a bug without a compile error.First, var rate = 18; makes an int, so later arithmetic uses integer division and drops decimals. Second, var list = new ArrayList<>(); makes an ArrayList<Object>, which compiles until you call a String method on an element, and silently accepts any type until then. Both are fixed by making the type explicit on the right side.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson