Twenty-five lines for two fields
Riya needs a Dish class with a name and a price. By the time she has written the fields, constructor, getters, equals, hashCode and toString, it's 25 lines, and she forgot to update equals when she added a veg field. Her JSON test data is a wall of \" and +.
Sir, this class only carries two values. Why so much code?
Since Java 16 you write record Dish(String name, int price) { } and the compiler generates all of it, and keeps equals in sync forever.
Anything for my messy JSON strings?
Text blocks. And var for those long generic types. Three small features, a lot less typing.
What are var, records and text blocks?
var (Java 10) declares a local variable whose static type is inferred by the compiler from its initializer; it is not a keyword for dynamic typing and cannot be used for fields, parameters or variables without an initializer. A record (Java 16) is a restricted, implicitly final class that is a transparent carrier for immutable data: its header declares components, and the compiler generates private final fields, a canonical constructor, accessor methods named after the components, and equals, hashCode and toString based on all components. A text block (Java 15) is a multi-line string literal delimited by """, in which incidental indentation is removed and quotes need no escaping.
var: “you know the type, compiler, you write it”. Record: “this is just data; give me the usual methods”. Text block: “keep my text exactly as I typed it”.
Words you'll meet in this lesson
price.Class vs record
| Ordinary class | Record | |
|---|---|---|
| Fields | You declare them; any modifiers | From the header; always private final |
| Constructor, getters | You write them | Generated (name(), not getName()) |
| equals / hashCode / toString | Object's (identity) unless you override | Generated from all components |
| Setters / mutable state | Allowed | Not possible |
| Inheritance | Can extend a class | Final; can only implement interfaces |
| Best for | Objects with behaviour and changing state | DTOs, values, keys, results |
Exam tip: “A record is a final, immutable data class: the compiler generates private final fields, a canonical constructor, accessors, equals, hashCode and toString. var is local type inference: the variable is still statically typed.” Mention the versions: var 10, text blocks 15, records 16.
The three features in action
Records
record Order(String id, int amount) { }
public class Main {
public static void main(String[] args) {
Order a = new Order("A1", 500);
Order b = new Order("A1", 500);
System.out.println(a);
System.out.println("id: " + a.id() + ", amount: " + a.amount());
System.out.println("equals: " + a.equals(b) + ", same object: " + (a == b));
}
}var: what's allowed
var count = 0; // int
var name = "Asha"; // String
var orders = new ArrayList<Order>(); // ArrayList<Order>
for (var o : orders) { ... } // Order
var x; // ✗ no initializer: can't infer
var y = null; // ✗ null has no type
var z = {1, 2}; // ✗ array initializer needs a type
private var field; // ✗ only local variablesText blocks
public class Main {
public static void main(String[] args) {
String old = "{\n" +
" \"item\": \"dosa\",\n" +
" \"price\": 80\n" +
"}";
String block = """
{
"item": "dosa",
"price": 80
}""";
System.out.println(block);
System.out.println("same text? " + old.equals(block));
}
}Records for DTOs
API requests and responses, rows from a query, results returned from a method.
data carriersRecords as keys
Value-based equals and hashCode make them perfect HashMap keys and Set elements.
equals by valueText blocks
JSON, SQL, HTML, email templates and test data, readable and copy-paste friendly.
""" … """var
Long generic types, loop variables, and obvious initializers like new ArrayList<>().
Use var when the type is obvious from the right-hand side. var orders = new ArrayList<Order>(); is clear; var x = service.process(data); hides what x is from the reader. Readability comes first. And remember record accessors are name(), not getName().
What happens at new Order("A1", 500) for a record
The compiler has generated the class
A final class extendingrecord Order(String id, int amount) { Order { if (amount < 0) throw new IllegalArgumentException(); } }java.lang.Record, with fieldsidandamount, accessors, equals, hashCode and toString.Allocate the object
newcreates the object on the heap with default field values.Run the compact constructor
Your validation runs first, with the parametersidandamount. If it throws, no Order is ever handed out.Assign the fields
At the end of the compact constructor the compiler-added code assignsthis.id = id; this.amount = amount;. The fields are final: they can never change again.Use it
o.amount()reads the field;o.equals(other)compares both components;o.toString()givesOrder[id=A1, amount=500].
Less boilerplate: both views
Left: the old code and the modern version side by side. Right: what the compiler generates for a record, and what the objects look like on the heap.
- A class that only carries data (an order id and amount) needs fields, a constructor, getters,
equals,hashCodeandtoString: about 25 lines of boilerplate. - A record declares the same thing in one line. The compiler generates all of it, and the fields are final: records are immutable.
- Long generic types often appear twice in one declaration.
varlets the compiler infer the type of a local variable from its initializer. Java is still statically typed:byCityis aHashMap<String, List<Order>>.- Multi-line text such as JSON, SQL or HTML used to need
\n, escaped quotes and+on every line. - A text block (
""") keeps the text as you see it: no escapes for quotes, real line breaks, and the common indentation removed.
- The compiler turns
record Order(String id, int amount)into a final class that extendsjava.lang.Record. Records can implement interfaces but can't extend other classes. - Each component becomes a
private finalfield. There are no setters: once built, a record never changes. - A canonical constructor takes all components; accessors are named after the components,
id()andamount(), notgetId(). toString()prints the name and all components:Order[id=A1, amount=500].equalsandhashCodecompare the component values, so two records with the same data are equal (but still different objects). That makes records safe as HashMap keys.vargenerates nothing at run time: the compiler writes the inferred type into the bytecode. Hover over avarin an IDE and you'll see the real type.
Modern Java in real projects
New Spring Boot and microservice code uses records for request and response objects, var for locals, and text blocks for queries and templates.
Watch it with an example: an order API response built with records and a text block
An online shop's API turns order records into a JSON response. Records hold the data; a text block holds the JSON template.
- Each order is a record: id and amount, immutable once created.
- A compact constructor in the record validates the amount.
new Order("A3", -5)throws, so a bad order can never exist. varkeeps the local declaration short; the stream sums the amounts using the generated accessorOrder::amount.- The JSON template is a text block: quotes need no escaping, and
formattedfills in the numbers. - The response is ready to send. (Real APIs usually let a library like Jackson turn records into JSON, but text blocks are perfect for SQL, templates and test data.)
1. API request/response objects
record CreateOrderRequest(String customerId, List<Item> items): JSON libraries map JSON to records and back.
2. Query results and SQL
Repository methods return records like record CitySales(String city, long total); the SQL itself sits in a readable text block.
3. Value objects
Money, Pincode, PhoneNumber, DateRange: small records with validation in the compact constructor prevent invalid values everywhere.
4. Templates and test data
Email bodies, SMS templates and JSON fixtures for tests are written as text blocks and filled with formatted.
Simplified: for user-supplied values in SQL, always use PreparedStatement parameters rather than formatting values into the text.
A menu card with records, var and a text block
import java.util.*;
record Dish(String name, int price, boolean veg) { }
public class Main {
public static void main(String[] args) {
var menu = List.of(new Dish("Paneer Tikka", 220, true), new Dish("Chicken Biryani", 260, false),
new Dish("Masala Dosa", 90, true));
var vegCount = menu.stream().filter(Dish::veg).count();
var cheapest = menu.stream().min(Comparator.comparingInt(Dish::price)).orElseThrow();
var card = """
-- Today's menu --
dishes: %d (veg: %d)
cheapest: %s at Rs %d
""".formatted(menu.size(), vegCount, cheapest.name(), cheapest.price());
System.out.print(card);
System.out.println(cheapest);
}
}record Dish(String name, int price, boolean veg)Accessorsname(),price(),veg()are generated.filter(Dish::veg)The boolean accessor works directly as a Predicate.var cheapest = …orElseThrow()The inferred type isDish.""" … """.formatted(...)A template with placeholders, filled likeprintf.
Two equal records and a text block
Recorded from the real JVM. Watch the compact constructor run for each record, two separate Order objects appear on the heap, and the text block become one multi-line String.
Worked examples: real problems, solved and traced in memory
Three ways these features make everyday code shorter and safer: a record as an immutable value with validation, records as map keys, and a text block for a formatted receipt.
A Money record that can't go wrong
Problem statement
A wallet app passes amounts around as plain ints and sometimes mixes paise with rupees or allows negatives. Create a Money record (paise) that rejects negatives, can add another Money, and prints in rupees.
₹120.50 + ₹79.50, and an invalid −₹10
Rs 120.50 + Rs 79.50 = Rs 200.00 rejected: money can't be negative: -1000
Approach
record Money(long paise)with a compact constructor that rejects negatives.- A method
plus(Money other)returns a new Money (records are immutable). - Override
toStringto show rupees.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Records as map keys
Problem statement
A sales report counts orders per (city, month). Instead of building string keys like "Pune-10", use a record CityMonth(String city, int month) as the HashMap key.
orders: Pune Oct, Pune Oct, Mumbai Oct, Pune Nov
{CityMonth[city=Mumbai, month=10]=1, CityMonth[city=Pune, month=10]=2, CityMonth[city=Pune, month=11]=1}
Pune in October: 2Approach
record CityMonth(String city, int month): equals/hashCode come free.merge(key, 1, Integer::sum)counts per key.- A new record with the same values finds the same entry.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
A formatted receipt with a text block
Problem statement
A shop prints a receipt: header, item lines, total and a thank-you line. Build it with a text block template and formatted, using records for the items.
2 × Dosa ₹80, 1 × Coffee ₹40
===== CANTEEN 21 ===== Dosa x2 Rs 160 Coffee x1 Rs 40 ---------------------- TOTAL Rs 200 Thank you, visit again!
Approach
record Item(String name, int qty, int price)with atotal()method.- Build the item lines in a loop with
String.format. - Insert them into a text block template with
formatted.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 14.5 in one screen
var: local variables only, initializer required, type fixed at compile time.- Record:
record Name(Type a, Type b) { }: final, immutable, data-only class. - Generated: private final fields, canonical constructor, accessors
a(), equals, hashCode, toString. - Compact constructor
Name { … }validates before fields are set. - Records may have methods and static members, and implement interfaces; no extra instance fields, no extends.
- Text block:
"""+ newline …"""; common indentation removed; quotes unescaped; useformatted. - Versions: var 10, text blocks 15, records 16.
Interview and viva questions
Is Java dynamically typed now because of var?
No. The compiler infers the type from the initializer at compile time, and the variable keeps that type forever; assigning a different type is a compile error. var only removes the need to write the type.
What does the compiler generate for a record?
A final class extending java.lang.Record with private final fields for each component, a public canonical constructor, public accessor methods named after the components, and equals, hashCode and toString implementations based on all components. You can add methods, static fields, a compact or custom constructor, and implement interfaces.
Record vs Lombok @Data / a normal class?
Records are a language feature (no library), always immutable and final, with value-based equality. A normal class or Lombok's @Data can have mutable fields, setters and inheritance. Use records for plain data; use classes when an object has changing state or rich behaviour.
Are records completely immutable?
Their fields are final, but if a component is a mutable object (like an ArrayList), its contents can still change. Make defensive copies in the compact constructor, e.g. items = List.copyOf(items);.
How are text blocks different from normal strings?
They span multiple lines, don't need escaped quotes or \n, and strip incidental indentation. At run time they're ordinary String objects; the processing happens at compile time.
Practice problems
Use var, records and text blocks to write the same programs with less code. Every output is fixed. The last three are business tasks.
A simple record
Declare record Student(String name, int marks), create one and print it and its marks.
Asha, 88
Student[name=Asha, marks=88] -> marks 88
Show solution and explanation
record Student(String name, int marks) { }
public class Main {
public static void main(String[] args) {
Student s = new Student("Asha", 88);
System.out.println(s + " -> marks " + s.marks());
}
}How it works: the record gets a constructor, name() and marks() accessors, and a readable toString automatically.
var with collections
Use var for a list of prices and a loop variable, and print the total.
120, 80, 45
total 245 from a ArrayList
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
var prices = new ArrayList<>(List.of(120, 80, 45));
var total = 0;
for (var p : prices) total += p;
System.out.println("total " + total + " from a " + prices.getClass().getSimpleName());
}
}How it works: prices is an ArrayList<Integer>, total an int, p an Integer: all inferred, all fixed.
A text block
Print a 3-line address using a text block.
(none)
MyPhatshala Learning Centre 12, MG Road Pune 411001 lines: 3
Show solution and explanation
public class Main {
public static void main(String[] args) {
String address = """
MyPhatshala Learning Centre
12, MG Road
Pune 411001
""";
System.out.print(address);
System.out.println("lines: " + address.lines().count());
}
}How it works: the indentation shared by all lines (and the closing quotes) is removed. The block ends with a line break because the closing """ is on its own line.
Records are equal by value
Create two identical Point records and show equals, hashCode and == results; put both in a HashSet and print its size.
(3, 4) twice
true true false set size: 1
Show solution and explanation
import java.util.*;
record Point(int x, int y) { }
public class Main {
public static void main(String[] args) {
var a = new Point(3, 4);
var b = new Point(3, 4);
System.out.println(a.equals(b) + " " + (a.hashCode() == b.hashCode()) + " " + (a == b));
System.out.println("set size: " + new HashSet<>(List.of(a, b)).size());
}
}How it works: records compare by value, so the set treats them as one element. == still compares references.
Compact constructor validation
A Pincode record must be exactly 6 digits. Validate in a compact constructor and try a good and a bad value.
411001, 41100
Pincode[value=411001] bad pincode: 41100
Show solution and explanation
record Pincode(String value) {
Pincode {
if (!value.matches("\\d{6}")) throw new IllegalArgumentException("bad pincode: " + value);
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new Pincode("411001"));
try {
new Pincode("41100");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}How it works: a compact constructor has no parameter list; it runs before the fields are assigned. Validating there means an invalid Pincode can't exist anywhere in the program.
Text block with formatted
Build an SMS from a text block template with name, amount and date placeholders.
Ravi, 1,499, 16-Oct
Hi Ravi, your payment of Rs 1,499 on 16-Oct was successful. Ref: TXN8841
Show solution and explanation
public class Main {
public static void main(String[] args) {
String sms = """
Hi %s, your payment of Rs %,d on %s was successful.
Ref: %s""".formatted("Ravi", 1499, "16-Oct", "TXN8841");
System.out.println(sms);
}
}How it works: %,d adds thousands separators. With the closing quotes right after the last line, the block has no trailing line break.
Invoice lines as records
Represent invoice lines as records with a lineTotal() method; print each line and the invoice total.
3 lines
Notebook 5 x 40 = 200 Pen 10 x 10 = 100 Bag 1 x 650 = 650 Invoice total: Rs 950
Show solution and explanation
import java.util.*;
record Line(String item, int qty, int rate) {
int lineTotal() { return qty * rate; }
}
public class Main {
public static void main(String[] args) {
var lines = List.of(new Line("Notebook", 5, 40), new Line("Pen", 10, 10), new Line("Bag", 1, 650));
var total = 0;
for (var l : lines) {
System.out.printf("%-9s %2d x %3d = %4d%n", l.item(), l.qty(), l.rate(), l.lineTotal());
total += l.lineTotal();
}
System.out.println("Invoice total: Rs " + total);
}
}How it works: records can have methods; they just can't have extra instance fields or change their components.
SQL query as a text block
Store a readable SQL query in a text block and print it with the city filled in.
city Pune
SELECT c.name, SUM(o.amount) AS total FROM customers c JOIN orders o ON o.customer_id = c.id WHERE c.city = 'Pune' GROUP BY c.name ORDER BY total DESC
Show solution and explanation
public class Main {
public static void main(String[] args) {
String sql = """
SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.city = '%s'
GROUP BY c.name
ORDER BY total DESC""".formatted("Pune");
System.out.println(sql);
}
}How it works: text blocks make embedded SQL readable and easy to copy into a database tool. (In real code, pass values with PreparedStatement parameters instead of formatting them in, to prevent SQL injection.)
Immutable config with a record
Load app settings into a Config record with defaults, and create a modified copy for a test environment (a “wither”).
prod and test
Config[env=prod, timeoutSec=30, sandbox=false] Config[env=test, timeoutSec=30, sandbox=true]
Show solution and explanation
record Config(String env, int timeoutSec, boolean sandbox) {
Config withEnv(String newEnv) {
return new Config(newEnv, timeoutSec, newEnv.equals("test"));
}
}
public class Main {
public static void main(String[] args) {
var prod = new Config("prod", 30, false);
var test = prod.withEnv("test");
System.out.println(prod);
System.out.println(test);
}
}How it works: since records can't change, a “with” method returns a modified copy. Immutable config objects are safe to share across threads.
Real-world scenario questions (asked in company interviews)
Modern-Java questions check whether you use these features where they help, and know their limits.
Duplicate products in a HashSet
A Product class with id and name is put into a HashSet to remove duplicates, but duplicates remain. A teammate added a new field last week. How would records help?
The interviewer is checking: equals/hashCode contracts and records.
Show a strong answer
- The class either never overrode equals/hashCode or they weren't updated with the new field, so equal products look different to the set.
- Make it
record Product(String id, String name, …): equals and hashCode are generated from all components and stay in sync whenever a component is added. - If only
idshould define equality, a record isn't the right fit; keep a class with an explicit equals on id.
Avoid: converting to a record without checking which fields should define equality.
A “record” whose list keeps changing
record Cart(String user, List<Item> items) is shared between threads. Someone calls cart.items().add(item) and another thread sees a half-updated cart. Records are immutable; how did this happen?
The interviewer is checking: shallow immutability.
Show a strong answer
- The
itemsfield can't be reassigned, but the ArrayList it points to is mutable. - In a compact constructor:
items = List.copyOf(items);, an unmodifiable copy. Adding an item then means creating a new Cart.
Avoid: adding synchronized everywhere instead of making the data truly immutable.
Should we ban var?
Your team lead says “var makes code unreadable, let's ban it”. Another developer uses var for everything, including var result = engine.run(input);. What guideline would you propose?
The interviewer is checking: balanced judgement.
Show a strong answer
- Use var when the type is obvious from the right side (
new, literals, well-named factory methods) or very long; avoid it when the type is unclear from the line. - Choose good variable names (
var pendingOrders), and keep methods short so inferred types are easy to follow. - Never use var to hide a surprising type (e.g. a
longvsintin calculations).
Avoid: “always” or “never”.
Text block + formatted + user input
A search API builds SQL with a text block: """SELECT * FROM orders WHERE city = '%s'""".formatted(cityFromUser). It reads nicely. Any concern?
The interviewer is checking: SQL injection awareness.
Show a strong answer
- Formatting user input into SQL allows injection:
' OR '1'='1returns every order. - Keep the text block for readability, but use
?placeholders andPreparedStatement.setString(or named parameters in your framework).
Avoid: trying to escape quotes yourself.