var, records and text blocks

Java used to be famous for long code. Recent versions fixed much of that. var stops you writing the same type twice, records turn a 25-line data class into one line, and text blocks let you write JSON, SQL and receipts exactly as they look. You'll learn what each feature generates, the rules that keep them safe, and where they shine in real projects.

Module 14 · Modern Java⏱ 22 minIntermediate9 practice problems
1 The story

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 +.

class Dish { private final String name; private final int price; Dish(String n, int p) {...} getName() {...} getPrice() {...} equals(Object o) {...} hashCode() {...} toString() {...} } record Dish(String name, int price) { } same behaviour, generated by the compiler myphatshala.in
Riya

Sir, this class only carries two values. Why so much code?

Arjun Sir

Since Java 16 you write record Dish(String name, int price) { } and the compiler generates all of it, and keeps equals in sync forever.

Riya

Anything for my messy JSON strings?

Arjun Sir

Text blocks. And var for those long generic types. Three small features, a lot less typing.

2 Definition

What are var, records and text blocks?

Formal definition (write this in exams)

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.

In simple words

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

Type inferenceThe compiler works out the type from the right-hand side.
Record componentA name in the record header, like price.
Canonical constructorThe constructor taking all components, in order.
Compact constructorA record constructor without parameters, for validation.
ImmutableCan't change after creation.
Incidental whitespaceIndentation in a text block that's removed automatically.

Class vs record

Ordinary classRecord
FieldsYou declare them; any modifiersFrom the header; always private final
Constructor, gettersYou write themGenerated (name(), not getName())
equals / hashCode / toStringObject's (identity) unless you overrideGenerated from all components
Setters / mutable stateAllowedNot possible
InheritanceCan extend a classFinal; can only implement interfaces
Best forObjects with behaviour and changing stateDTOs, 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.

3 Types and ways to use it

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));
    }
}
Order[id=A1, amount=500] id: A1, amount: 500 equals: true, same object: false

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 variables

Text 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));
    }
}
{ "item": "dosa", "price": 80 } same text? true

Records for DTOs

API requests and responses, rows from a query, results returned from a method.

data carriers

Records as keys

Value-based equals and hashCode make them perfect HashMap keys and Set elements.

equals by value

Text 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<>().

local only

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().

4 Step by step

What happens at new Order("A1", 500) for a record

  1. The compiler has generated the class

    record Order(String id, int amount) {
        Order { if (amount < 0) throw new IllegalArgumentException(); }
    }
    A final class extending java.lang.Record, with fields id and amount, accessors, equals, hashCode and toString.
  2. Allocate the object

    new creates the object on the heap with default field values.
  3. Run the compact constructor

    Your validation runs first, with the parameters id and amount. If it throws, no Order is ever handed out.
  4. Assign the fields

    At the end of the compact constructor the compiler-added code assigns this.id = id; this.amount = amount;. The fields are final: they can never change again.
  5. Use it

    o.amount() reads the field; o.equals(other) compares both components; o.toString() gives Order[id=A1, amount=500].
5 See it happen

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.

Less boilerplate: both views 6 steps
What you see final class Order { private final String id; private final int amount; Order(String id, int amount) {...} String id() {...} int amount() {...} equals(...) {...} hashCode() {...} toString() {...} // ~25 lines record Order(String id, int amount) { } // constructor, accessors, equals, // hashCode, toString: generated Map<String, List<Order>> byCity = new HashMap<String, List<Order>>(); var byCity = new HashMap<String, List<Order>>(); // the type is still HashMap<…>, // inferred at compile time String json = "{\n" + " \"id\": \"A1\",\n" + " \"amount\": 500\n" + "}"; String json = """ { "id": "A1", "amount": 500 } """; before: a data class record: same behaviour before: type written twice var: type written once before: escapes and + text block: what you see Java 16 Java 10 Java 15 A plain data-holder class. One line with a record. Long generic types, twice. var: let the compiler infer. JSON as escaped strings. A text block: readable. myphatshala.in
Behind the screen Order.class (generated) final class Order extends Record private final String id; private final int amount; Order(String id, int amount) String id() int amount() equals / hashCode / toString HEAP #1 Order id="A1" amount=500 #2 Order id="A1" amount=500 o.toString() Order[id=A1, amount=500] o.equals(p) → true o == p → false var o = new Order(...) type of o: Order record = class with less typing A final class extending Record. One private final field per component. Canonical constructor and accessors. toString is generated. equals compares values. var: types fixed at compile time. myphatshala.in
6 Real-world use

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.

Records in, JSON out 5 steps
Animated example Order("A1", 450) Order("A2", 1200) Order("A3", -5) compact constructor: amount must be ≥ 0 rejected before it ever existed var total = orders.stream() .mapToInt(Order::amount) .sum(); // 1650 """ { "orders": %d, "total": %d } """.formatted(2, total) { "orders": 2, "total": 1650 } Two valid orders as records. Invalid data is rejected. Total with var and a stream. Fill a text-block template. The JSON response. myphatshala.in
{"id":…}

1. API request/response objects

record CreateOrderRequest(String customerId, List<Item> items): JSON libraries map JSON to records and back.

POST /orders→record CreateOrderRequest→service

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.

"""SELECT …"""→List<CitySales>→report
₹

3. Value objects

Money, Pincode, PhoneNumber, DateRange: small records with validation in the compact constructor prevent invalid values everywhere.

new Money(-10)→compact constructor→rejected

4. Templates and test data

Email bodies, SMS templates and JSON fixtures for tests are written as text blocks and filled with formatted.

template→""" … """.formatted(...)→message

Simplified: for user-supplied values in SQL, always use PreparedStatement parameters rather than formatting values into the text.

7 Run the code

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);
    }
}
-- Today's menu -- dishes: 3 (veg: 2) cheapest: Masala Dosa at Rs 90 Dish[name=Masala Dosa, price=90, veg=true]
  • record Dish(String name, int price, boolean veg)Accessors name(), price(), veg() are generated.
  • filter(Dish::veg)The boolean accessor works directly as a Predicate.
  • var cheapest = …orElseThrow()The inferred type is Dish.
  • """ … """.formatted(...)A template with placeholders, filled like printf.
8 Line by line

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.

Example 1Banking · money

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.

Input
₹120.50 + ₹79.50, and an invalid −₹10
Expected output
Rs 120.50 + Rs 79.50 = Rs 200.00
rejected: money can't be negative: -1000

Approach

  1. record Money(long paise) with a compact constructor that rejects negatives.
  2. A method plus(Money other) returns a new Money (records are immutable).
  3. Override toString to 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.

Example 2Analytics · grouping

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.

Input
orders: Pune Oct, Pune Oct, Mumbai Oct, Pune Nov
Expected output
{CityMonth[city=Mumbai, month=10]=1, CityMonth[city=Pune, month=10]=2, CityMonth[city=Pune, month=11]=1}
Pune in October: 2

Approach

  1. record CityMonth(String city, int month): equals/hashCode come free.
  2. merge(key, 1, Integer::sum) counts per key.
  3. 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.

Example 3Retail · receipts

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.

Input
2 × Dosa ₹80, 1 × Coffee ₹40
Expected output
===== CANTEEN 21 =====
Dosa     x2  Rs  160
Coffee   x1  Rs   40
----------------------
TOTAL          Rs  200
Thank you, visit again!

Approach

  1. record Item(String name, int qty, int price) with a total() method.
  2. Build the item lines in a loop with String.format.
  3. 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.

9 Quiz and revision

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; use formatted.
  • 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.

P1Easy

A simple record

Declare record Student(String name, int marks), create one and print it and its marks.

Input
Asha, 88
Expected output
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());
    }
}
Student[name=Asha, marks=88] -> marks 88

How it works: the record gets a constructor, name() and marks() accessors, and a readable toString automatically.

P2Easy

var with collections

Use var for a list of prices and a loop variable, and print the total.

Input
120, 80, 45
Expected output
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());
    }
}
total 245 from a ArrayList

How it works: prices is an ArrayList<Integer>, total an int, p an Integer: all inferred, all fixed.

P3Easy

A text block

Print a 3-line address using a text block.

Input
(none)
Expected output
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());
    }
}
MyPhatshala Learning Centre 12, MG Road Pune 411001 lines: 3

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.

P4Medium

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.

Input
(3, 4) twice
Expected output
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());
    }
}
true true false set size: 1

How it works: records compare by value, so the set treats them as one element. == still compares references.

P5Medium

Compact constructor validation

A Pincode record must be exactly 6 digits. Validate in a compact constructor and try a good and a bad value.

Input
411001, 41100
Expected output
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());
        }
    }
}
Pincode[value=411001] bad pincode: 41100

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.

P6Medium

Text block with formatted

Build an SMS from a text block template with name, amount and date placeholders.

Input
Ravi, 1,499, 16-Oct
Expected output
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);
    }
}
Hi Ravi, your payment of Rs 1,499 on 16-Oct was successful. Ref: TXN8841

How it works: %,d adds thousands separators. With the closing quotes right after the last line, the block has no trailing line break.

P7Business problem

Invoice lines as records

Represent invoice lines as records with a lineTotal() method; print each line and the invoice total.

Input
3 lines
Expected output
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);
    }
}
Notebook 5 x 40 = 200 Pen 10 x 10 = 100 Bag 1 x 650 = 650 Invoice total: Rs 950

How it works: records can have methods; they just can't have extra instance fields or change their components.

P8Business problem

SQL query as a text block

Store a readable SQL query in a text block and print it with the city filled in.

Input
city Pune
Expected output
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);
    }
}
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

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.)

P9Business problem

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”).

Input
prod and test
Expected output
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);
    }
}
Config[env=prod, timeoutSec=30, sandbox=false] Config[env=test, timeoutSec=30, sandbox=true]

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.

Bug · equalsFresher

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 id should 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.

Design · immutabilityJunior

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 items field 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.

Code review · varJunior

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 long vs int in calculations).

Avoid: “always” or “never”.

Security · SQLMid

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'='1 returns every order.
  • Keep the text block for readability, but use ? placeholders and PreparedStatement.setString (or named parameters in your framework).

Avoid: trying to escape quotes yourself.