toString
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In Lesson 1 you saw Account@4517d9a3 appear when you printed an object. That text came from the default toString() inherited from Object. In this lesson you replace it with a description a human can read, and you learn the rule that matters most in real systems: a toString must never reveal secrets.
Back to the banknotes: the serial number on a note is useful to the bank, but you would not print the PIN of a bank card on a receipt. A good description says what something is and which values identify it, and nothing more.
Why this matters: toString is the text that appears in logs, error messages, and the debugger. When a service fails at night, that text is often the only clue you have. It is also the text that can leak a customer's data into a log file that many people can read.
This lesson builds on Lessons 1 to 3, on string concatenation, and on super calls from the inheritance module.
Learning Objectives
By the end of this lesson you will be able to:
- Override
toString()with the correct signature. - Explain how
printlnand string concatenation calltoStringautomatically. - Choose the content of a description: class name and key fields.
- Keep secrets out of
toString, and mask data when part of it is useful. - Reuse the parent's description in a subclass with
super.toString(). - Explain what happens when you print
null.
Written Explanation
How toString gets called
toString() returns a String. You rarely call it by hand, because Java calls it for you whenever it needs an object as text:
System.out.println(obj)prints the text fromobj.toString()."Opened " + obj(concatenation) uses it for the object side.- Logging libraries and the debugger use it to show object values.
Because the method lives in Object, every class has it, and a class that does not override it gets the default ClassName@hexNumber. The Java documentation recommends that every class override toString.
Writing one
The signature is exactly public String toString(). Write @Override so a typo becomes a compile error:
class Account {
private final String number;
private double balance;
private final String pin;
// constructor omitted
@Override
public String toString() {
return "Account[" + number + ", balance=" + balance + "]";
}
}The pin field exists but is deliberately not included.
What to include
A useful description answers "what is this, and which one?":
- the class name, so the log line is self-explanatory;
- the key fields, such as an id, number, or the values that define equality;
- short and on one line, since logs are read line by line.
Keep it free of side effects: toString should only build text, not change state, call a database, or throw exceptions. A toString that can fail turns a logging line into a new bug.
What must stay out
Never include in toString:
- passwords, PINs, security answers, tokens, API keys;
- full card numbers or other government or financial identifiers;
- large amounts of data, such as an entire list of transactions (the log will be huge).
When part of a value is useful, mask it: show only the last four digits of a card, for example. Remember that code which prints account for debugging will do it in production too, and logs are copied to many systems. Keeping secrets and personal data out of logs is standard security practice.
Designing the format
Pick one style and use it in every class of a project, so logs look uniform and are easy to search. A common style is ClassName[field=value, field=value]. Some practical points:
- Put fields in a fixed order, the same as the declaration order, so two lines can be compared by eye.
- Keep each description on a single line. A line break inside
toStringbreaks tools that read logs line by line. - Include the fields that define equality (Lesson 3) first. When two objects look the same in the log, you can tell whether they are "equal" at a glance.
- Do not try to make
toStringa data format. If another program needs the data, a later module teaches proper formats such as JSON;toStringis for people.
Subclasses
A subclass inherits toString and can extend it with super.toString():
class SavingsAccount extends Account {
private final double rate;
// constructor omitted
@Override
public String toString() {
return super.toString() + " rate=" + rate;
}
}This reuses the parent's text and adds the new field, so each class describes only what it owns.
Printing null
println(null reference) and "x" + nullReference do not throw: the text null appears. But calling nullReference.toString() directly throws NullPointerException. String.valueOf(obj) and Objects.toString(obj) return "null" for a null reference and obj.toString() otherwise.
toString and the debugger
When you stop a program in a debugger, the variables panel usually shows each object with its toString() text next to the name. With the default, you see only Account@4517d9a3 and must open the object to inspect fields one by one. With a good override you can scan a whole list of objects at a glance. The exact appearance depends on your IDE, so check how yours displays objects, but the benefit is the same everywhere.
toString is not equals
Two objects with the same toString text are not necessarily equal, and two equal objects can print differently if you add a field that equals ignores. Never compare objects by comparing their text (a.toString().equals(b.toString())): it is slow, fragile, and it breaks as soon as someone changes the format. Use equals for equality and keep toString for reading.
IDE-generated versions
Most IDEs can generate toString. The menu name and location differ by IDE and version, so look in your IDE's "generate" or "source" actions. A generated version usually lists every field, so always read it and delete anything sensitive.
Examples
Example 1: Account with a readable description (simple)
Account a = new Account("A-1", 250.5, "1234");
System.out.println(a);
System.out.println("Opened " + a);Real output:
Account[A-1, balance=250.5]
Opened Account[A-1, balance=250.5]Both lines called toString() without us writing the call. The PIN 1234 does not appear.
Example 2: a subclass extending the text (practical)
System.out.println(new SavingsAccount("S-2", 1000, "9999", 0.03));Account[S-2, balance=1000.0] rate=0.03super.toString() supplied the first part and the subclass added its own field. If the subclass had no toString, it would print only the Account part, which is correct but incomplete.
Example 3: masking a card number (realistic)
class Card {
private final String pan;
Card(String pan) { this.pan = pan; }
@Override public String toString() {
return "Card[**** **** **** " + pan.substring(pan.length() - 4) + "]";
}
}
Card card = new Card("4111111111111111");
System.out.println("Payment with " + card + " for " + a);
Account none = null;
System.out.println("Account: " + none);
System.out.println(String.valueOf(none));Payment with Card[**** **** **** 1111] for Account[A-1, balance=250.5]
Account: null
nullThe log line is enough to recognize the card with the customer without exposing the full number (4111... is a standard test number, not a real one). Calling none.toString() directly would throw NullPointerException, which we confirmed by running it. A class with no override prints as Plain@7229724f.
Backend Insight
In a backend, the first debugging tool is the log. Compare two log lines:
Order failed for com.shop.Order@5ca881b5Order failed for Order[id=1042, customer=77, total=250.50 SAR]
The second tells a tired engineer what happened in seconds. That is the value of a good toString.
The same line is a security risk if you are careless. Logs are stored, copied to monitoring tools, and read by many people, and a password or card number that reaches a log can be exposed for a long time. In a regulated industry such as banking or payments, logging full card numbers can also break rules your company has to follow. The safe default: log identifiers, not secrets. Later modules on logging and security build on this habit.
A small habit that pays off: after writing toString, print one object and read the line as if you were the on-call engineer. Can you tell what it is, which one it is, and is there anything in it you would not want in a shared log? If the answer to the last question is yes, remove it before the code is merged.
Common Mistakes
Mistake 1: Including sensitive fields
- Incorrect:
"User[" + name + ", password=" + password + "]" - Correct: leave the password out entirely.
Mistake 2: Typing the wrong signature
public String tostring() or public String toString(int x) does not override anything. @Override turns that into a compile error.
Mistake 3: Letting toString do real work or fail
Querying a database, loading a file, or calling a method that can throw inside toString makes logging risky. Only read fields already in the object.
Mistake 4: Listing everything
Dumping a huge collection of nested objects produces enormous log lines. Include the identifying fields only.
Mistake 5: Parsing toString output
The text is for humans. If other code needs the values, use fields and getters, because the format may change.
Mistake 6: Exposing internals through nested objects
If a class holds another object, its toString text includes that object's toString. A masked card inside an order is fine; an unmasked one inside the same order leaks. Check every class involved, not only the outer one.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Why does System.out.println(account) show Account[A-1, balance=250.5] after you add toString?
أظهر الحل A1
A1. println calls toString() on the object, and the class now overrides it instead of using the default from Object.
A2. Write the exact method signature of toString.
أظهر الحل A2
A2. public String toString().
A3. Name two kinds of data that should never appear in toString.
أظهر الحل A3
A3. Any two of: passwords or PINs, tokens or API keys, full card numbers, government identifiers.
Level B: Application
B1. Write toString for Employee(String name, String email, String taxId) that shows the name and email but not the tax id, in the style Employee[name=..., email=...].
أظهر الحل B1
B1.
@Override
public String toString() {
return "Employee[name=" + name + ", email=" + email + "]";
}B2. You wrote public String toString(int x). What happens and what protects you from this mistake?
أظهر الحل B2
B2. It compiles but does not override anything: it is an unrelated overloaded method, so printing still uses the default text. Writing @Override makes the compiler report that the method does not override a method from a supertype.
B3. A class Student already has toString. SeniorStudent extends Student adds a field thesis. Write its toString so the output is the parent's text followed by thesis= and the value.
أظهر الحل B3
B3.
@Override
public String toString() {
return super.toString() + " thesis=" + thesis;
}Level C: Challenge
C1. A card class holds pan (a 16-digit string). Write toString that shows only the last four digits, and explain why pan.substring(pan.length() - 4) is better than substring(12) if some cards are 15 digits.
أظهر الحل C1
C1.
@Override
public String toString() {
return "Card[**** " + pan.substring(pan.length() - 4) + "]";
}substring(12) assumes length 16 and would show the wrong characters for a 15-digit card (and throw for shorter strings). Counting from the end works for any length of at least four.
C2. A colleague's toString calls database.loadCustomer(id) to print a customer's name. List two problems.
أظهر الحل C2
C2. (1) toString would do slow, failure-prone work (a database call) just to print a line, so logging can become slow or throw. (2) It has side effects and hidden dependencies: simply printing an object now hits the database, which is wasteful and can cause errors in places that have no database access.
Mini Challenge
Take the Book, Member, and Loan classes from Lesson 1 (or write any three small classes). Add a toString to each using the Name[field=value, ...] style. Give Member a private pin field and prove, by printing, that it never appears. Then print an array of the three objects in a loop and a null reference once.
Check your work: every line starts with the class name, no secret appears, and null prints as null without an exception.
Summary & Next Lesson
Key takeaways
toString()returns human-readable text;printlnand concatenation call it for you.- Write
@Override public String toString(). Include the class name and key fields. - Keep it short, side-effect free, and never include passwords, tokens, or full card numbers; mask partial values.
- Subclasses can build on
super.toString(). - Printing a
nullreference givesnull; callingtoString()on it throws.
Next lesson
The next lesson, "Records as a preview: equality for free", shows a modern Java feature that generates equals, hashCode, and toString for you.
Self-check
- Who calls
toString()when you write"Opened " + account? - Which fields belong in a
toString? - Why is it dangerous to include a password?
- What does
super.toString()do in a subclass? - What is printed for
String.valueOf(null reference)?
أظهر الإجابات
- Java calls it automatically when converting the object to text.
- The class name and the key identifying fields, never secrets.
- Logs are widely stored and read, so a password would be exposed.
- It returns the parent class's description, which the subclass can extend.
- The text
null.
References
- Oracle, Java SE 21 API:
java.lang.Object. It describes the defaulttoStringformat and recommends that all subclasses override it. - Oracle, Java SE 21 API:
java.util.Objects. It documentsObjects.toString(Object)returning"null"fornull. - The advice about secrets in logs is general security practice and was not checked against a specific standard here. The outputs shown were produced by compiling and running the code on JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.