Command Palette

Search for a command to run...

[Java Basics] Class và Object trong Java: nền tảng của lập trình hướng đối tượng

Class là một type do chính bạn định nghĩa. Nó nói đúng hai chuyện và không hơn: mỗi object thuộc type đó mang theo state gì, và object đó làm được gì. Object là một instance cụ thể dựng ra từ class đó — một vùng bộ nhớ riêng trên heap, giữ bản sao riêng của từng field.

Quan hệ đó là toàn bộ nội dung bài này. Một class, bao nhiêu object cũng được; giá trị thuộc về object, còn code thuộc về class. Mọi dòng output và mọi message lỗi bên dưới đều chạy thật trên OpenJDK 21.0.6.

Một class, nhiều object, mỗi object giữ bộ field riêng

Phạm vi: mọi object ở đây được tạo bằng default constructor ngầm định và mọi field đều để trơn, để một ý duy nhất trong tiêu đề được cả bài để giải thích.

Class và object thực sự là gì

Class là một định nghĩa type. Nó không giữ dữ liệu và nó không chạy. Nó là bản mô tả mà javac compile và JVM load, và nó trả lời hai câu hỏi: một cái thuộc loại này giữ gì, và một cái thuộc loại này làm được gì.

Object là một instance dựng ra từ bản mô tả đó. Nó nằm trên heap, nó sở hữu bản sao riêng của từng field mà class khai báo, và nó không sở hữu một dòng code method nào — code chỉ tồn tại một bản, nằm ở class.

ClassObject
Là cái gìđịnh nghĩa type, viết một lầnmột instance dựng ra từ nó
Có bao nhiêumột, JVM load một lầngọi new bao nhiêu lần thì bấy nhiêu
Giữkhai báo fieldgiá trị field
Code methodmột bản, dùng chungkhông có bản riêng nào
Nằm ở đâumetadata của class trong JVMheap
Viết ra saoclass Point { ... }new Point()

Không có chỗ nào ở trên là ẩn dụ để bạn phải tin suông. Với class Point được viết ở phần sau, getClass() trả về class mà object được dựng ra từ đó, và hai object cùng class trả về đúng một thứ:

Point home = new Point();
Point office = new Point();

System.out.println("home.getClass()   = " + home.getClass());
System.out.println("office.getClass() = " + office.getClass());
System.out.println("same class object -> " + (home.getClass() == office.getClass()));
System.out.println("same instance     -> " + (home == office));
System.out.println("declared fields   = " + Point.class.getDeclaredFields().length);
System.out.println("declared methods  = " + Point.class.getDeclaredMethods().length);
home.getClass()   = class Point
office.getClass() = class Point
same class object -> true
same instance     -> false
declared fields   = 3
declared methods  = 2

Hai object khác nhau, một class dùng chung. Class biết nó khai báo ba field và hai method; không object nào phải giữ bản sao riêng của hai thông tin đó.

Viết class đầu tiên

Thân của một class chứa field — phần state — và method — phần hành vi. Cả hai được gọi chung là member. Đây là class mà cả bài này chạy trên đó, ba field và hai method:

class Point {
    int x;
    int y;
    String label;

    void describe() {
        System.out.println(label + " is at (" + x + ", " + y + ")");
    }

    double distanceFromOrigin() {
        return Math.sqrt(x * x + y * y);
    }
}

Ba điểm đáng để ý. Field được khai báo y hệt local variable nhưng nằm thẳng trong thân class, không nằm trong method nào. Hai method không có static, nên chúng là instance method — chúng thuộc về một object và đọc field của object đó bằng tên trần. Và field không mang public hay private, tức là chúng đang ở mức package-private, đủ dùng cho một ví dụ một file và cũng chính là chỗ bài 26 thay bằng getter với setter.

Giờ dựng object từ nó và cho mỗi object một bộ giá trị khác nhau:

public class PointDemo {
    public static void main(String[] args) {
        Point home = new Point();
        home.x = 3;
        home.y = 4;
        home.label = "Home";

        Point office = new Point();
        office.x = 10;
        office.y = 2;
        office.label = "Office";

        Point gym = new Point();
        gym.x = 7;
        gym.y = 9;
        gym.label = "Gym";

        home.describe();
        office.describe();
        gym.describe();

        System.out.println("home is " + home.distanceFromOrigin() + " from the origin");
        System.out.println("gym is " + gym.distanceFromOrigin() + " from the origin");
    }
}
Home is at (3, 4)
Office is at (10, 2)
Gym is at (7, 9)
home is 5.0 from the origin
gym is 11.40175425099138 from the origin

Ba object, ba bộ giá trị, một thân describe(). home.xoffice.x là hai vùng bộ nhớ khác nhau; đoạn code trong describe() chỉ tồn tại một bản và cả ba lời gọi đều chạy vào đó.

Một định nghĩa class, ba object trên heap với bộ field riêng, và một method body duy nhất mà mọi lời gọi đều chạy

Dấu chấm làm hết phần việc: home.x đọc field của một object, còn home.describe() gọi method trên object đó. Trong describe() không có chỗ nào nhắc tới home, office hay gym — nó viết label, x, y, và những cái tên đó trỏ về đúng object mà lời gọi xuất phát.

Tạo object bằng new

new Point() là một expression nhưng làm nhiều việc theo một thứ tự cố định, và biết thứ tự đó giải thích được gần hết những chỗ gây bất ngờ trong bài này.

new làm gì, theo đúng thứ tự

  1. Allocate. JVM xin heap cho một Point, đủ chỗ cho ba field slot của nó.
  2. Zero-init. Mỗi slot được ghi giá trị default của type — trước khi bất kỳ dòng code nào của bạn chạy.
  3. Chạy constructor. Point không khai báo constructor nào, nên javac cấp một constructor no-argument ngầm định và cái đó chạy, không làm gì cả.
  4. Trả về reference. Expression new cho ra một reference tới object, không bao giờ là chính object.
  5. Assign. Reference đó được copy vào variable bên trái, một slot trong stack frame hiện tại.

new Point() tách thành allocate, zero-init, chạy constructor, trả về reference và assign, kèm giá trị zero in ra thật

Bước 3 là chỗ bài này cố tình để mỏng. Tự viết constructor, cho nó parameter, và overload nó là toàn bộ chủ đề của bài 24; từ giờ tới đó, mọi object bắt đầu cuộc đời với đúng những giá trị mà bước 2 đặt vào.

Field khởi đầu bằng giá trị zero, và in ra xem được

Bước 2 là bảo đảm của ngôn ngữ chứ không phải may rủi, và rất dễ nhìn thấy. Khai báo một class toàn field, tạo một object, rồi đọc field ra trước khi ghi bất cứ thứ gì:

class Reading {
    int count;
    long id;
    double celsius;
    boolean valid;
    char grade;
    String sensor;
    int[] samples;
}

public class ZeroInit {
    public static void main(String[] args) {
        Reading r = new Reading();
        System.out.println("int     count   = " + r.count);
        System.out.println("long    id      = " + r.id);
        System.out.println("double  celsius = " + r.celsius);
        System.out.println("boolean valid   = " + r.valid);
        System.out.println("char    grade   = code " + (int) r.grade);
        System.out.println("String  sensor  = " + r.sensor);
        System.out.println("int[]   samples = " + r.samples);
    }
}
int     count   = 0
long    id      = 0
double  celsius = 0.0
boolean valid   = false
char    grade   = code 0
String  sensor  = null
int[]   samples = null

Field số bắt đầu ở 0, booleanfalse, char ở code point 0, và mọi reference type — kể cả array — ở null. Bài 6 đã cho thấy đúng bộ default này; thông tin mới ở đây là lúc nào chúng được ghi, và câu trả lời là bên trong new, giữa bước allocate và bước constructor.

Local variable thì vẫn không được hưởng gì cả. Trong một method, int n; rồi đọc ra là lỗi compile, và việc tạo object nhiều tới đâu cũng không đổi được điều đó.

Reference variable không giữ object

Point home = new Point(); không nhét một Point vào home. Nó nhét một reference vào home, còn object nằm chỗ khác trên heap. Đây là bài học của bài 6 lặp lại, lần này trên một class do bạn viết, và đáng bỏ ra hai phút vì mọi cái bẫy còn lại trong bài đều mọc ra từ đó.

Hai stack slot cùng trỏ vào một Point trên heap, một lần ghi qua tên này đọc lại được qua tên kia, và một reference thứ ba giữ null

Hai cái tên, một object

Gán một reference variable cho một reference variable khác là copy reference, không copy object. Hai cái tên khi đó cùng chạm tới một object, và ghi qua tên nào cũng thấy được qua cả hai:

Point home = new Point();
home.x = 3;
home.y = 4;
home.label = "Home";

Point shortcut = home;

Point copyLooking = new Point();
copyLooking.x = 3;
copyLooking.y = 4;
copyLooking.label = "Home";

shortcut.x = 99;

System.out.println("home.x        = " + home.x);
System.out.println("shortcut.x    = " + shortcut.x);
System.out.println("copyLooking.x = " + copyLooking.x);
System.out.println();
System.out.println("home == shortcut    -> " + (home == shortcut));
System.out.println("home == copyLooking -> " + (home == copyLooking));
home.x        = 99
shortcut.x    = 99
copyLooking.x = 3

home == shortcut    -> true
home == copyLooking -> false

Một lần ghi, shortcut.x = 99, đã đổi luôn cái mà home.x đọc ra. copyLooking được dựng bằng new và khởi đầu với đúng ba giá trị đó, nhưng nó đứng yên — nó là một object khác, chỉ trùng hợp là nhìn giống hệt.

Đó cũng là ý nghĩa của == trên object: identity, không phải bằng nhau về nội dung. home == shortcuttrue vì hai variable giữ cùng một reference. home == copyLookingfalse ngay tại thời điểm field của chúng trùng nhau, vì chúng là hai object.

null, và NullPointerException nó thực sự sinh ra

null là giá trị hợp lệ của mọi reference variable và có nghĩa là variable đó không gọi tên object nào. Đọc nó thì được; dereference nó thì không:

Point ghost = null;
System.out.println("ghost = " + ghost);
System.out.println("ghost == null -> " + (ghost == null));
ghost.describe();
ghost = null
ghost == null -> true
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Point.describe()" because "ghost" is null
	at NullRef.main(NullRef.java:16)

Để ý dòng đầu. In một reference null ra không hề ném exception, vì nối chuỗi đi qua String.valueOf, và hàm này biến null thành bốn ký tự null. Chỉ lời gọi method mới ném.

Message này là một helpful NullPointerException, bật mặc định từ Java 15: nó gọi tên method không gọi được và đúng expression đang là null. Một chi tiết hay làm khó người compile bằng tay — tên của một local variable lấy từ debug information trong class file. javac -g NullRef.java cho ra because "ghost" is null như trên, còn javac NullRef.java trơn cho ra thế này:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Point.describe()" because "<local1>" is null
	at NullRef.main(NullRef.java:16)

Field thì được gọi tên trong cả hai trường hợp. IDE luôn compile kèm debug information, nên trên thực tế bạn sẽ thấy tên thật.

Instance method chạy trên object bạn gọi nó

Đây là chỗ lập trình hướng đối tượng không còn là một mớ từ vựng nữa. describe() có một thân duy nhất, compile một lần, và nó đọc label, x, y mà không kèm thêm gì phía trước. Những cái tên đó trỏ vào object nào là do chỗ gọi quyết định, cụ thể là do cái nằm bên trái dấu chấm:

home.describe();     // Home is at (3, 4)
office.describe();   // Office is at (10, 2)
gym.describe();      // Gym is at (7, 9)

Object bên trái dấu chấm gọi là receiver. Instance method nào cũng có một receiver, và mọi tên field trần trong thân method đều trỏ về nó. Đổi receiver thì cùng một dãy lệnh cho ra kết quả khác, và đó chính là lý do một class có field hữu ích hơn hẳn một đống static method mà lần nào gọi cũng phải chuyền tay đủ mọi giá trị.

Bài 19 đã giới thiệu nửa còn lại: static method không có receiver, nên nó không chạm được vào instance field, và cũng vì thế gọi describe() từ static main mà không có gì bên trái dấu chấm thì không compile được. Lỗi đó nằm ở phần lỗi thường gặp bên dưới.

toString(): object của bạn in ra cái gì

Đưa một object cho println thì Java gọi toString() trên nó. Nếu class của bạn không định nghĩa method đó, bản kế thừa từ Object sẽ chạy và in ra tên class, một dấu @, rồi một số hệ 16:

Point home = new Point();
home.x = 3; home.y = 4; home.label = "Home";

Point office = new Point();
office.x = 10; office.y = 2; office.label = "Office";

System.out.println(home);
System.out.println(office);
System.out.println("as text: " + home);
Point@15db9742
Point@6d06d69c
as text: Point@15db9742

Hai điều về con số hệ 16 đó. Nó là Integer.toHexString(hashCode()), và nó không phải địa chỉ bộ nhớ của object — JVM muốn tính kiểu gì cũng được. Chạy đúng chương trình đó với một thuật toán identity hash khác là thấy ngay:

$ java -XX:+UnlockExperimentalVMOptions -XX:hashCode=2 ToStringDemo
Point@1
Point@1
as text: Point@1

Cách sửa có ích là tự viết toString(). Đánh dấu @Override để compiler kiểm tra giùm rằng bạn đang thay một method kế thừa chứ không phải bịa ra một method mới:

class Point {
    int x;
    int y;
    String label;

    @Override
    public String toString() {
        return "Point[" + label + " (" + x + ", " + y + ")]";
    }
}
Point[Home (3, 4)]
as text: Point[Home (3, 4)]
a null reference: null

Dòng cuối là cùng chương trình đó in ra một Point đang null. Override toString() không đổi được điều này: String.valueOf kiểm tra null trước và không bao giờ gọi tới method của bạn. @Override nghĩa là gì, và override nói chung là chuyện gì, là chủ đề của bài 28; ở đây nó chỉ là một annotation mua cho bạn một lần kiểm tra chính tả lúc compile.

equals và == trên object

== trên hai reference variable hỏi xem chúng có giữ cùng một reference hay không. Nó không bao giờ nhìn vào field. equals là một method, nên nó nhìn field được — nhưng bản mà mọi class kế thừa từ Object thì không nhìn. Nó được định nghĩa là identity, nên mặc định equals== trả lời đúng một câu hỏi:

Point a = new Point();
a.x = 3; a.y = 4; a.label = "Home";

Point b = new Point();
b.x = 3; b.y = 4; b.label = "Home";

Point c = a;

System.out.println("a == b       -> " + (a == b));
System.out.println("a.equals(b)  -> " + a.equals(b));
System.out.println("a == c       -> " + (a == c));
System.out.println("a.equals(c)  -> " + a.equals(c));
a == b       -> false
a.equals(b)  -> false
a == c       -> true
a.equals(c)  -> true

ab có field giống hệt nhau mà vẫn không bằng nhau, vì trong Point chưa có chỗ nào nói bằng nhau đối với một điểm nghĩa là gì. Những class như String hay Integer cư xử khác chỉ vì bản thân chúng đã override equals.

⚠️ Override equals cho đúng khó hơn vẻ ngoài của nó: có một contract phải thỏa mãn, và hashCode phải được override kèm theo, nếu không mọi collection dựa trên hash sẽ chạy sai. Chuyện đó thuộc về khóa nâng cao, không phải ở đây. Từ giờ tới lúc đó, hoặc so sánh thẳng những field bạn quan tâm, hoặc dùng record.

Một class một file, hay nhiều class trong một file

Một file .java khai báo bao nhiêu class cũng được. Compile một file thì ra một file .class cho mỗi class:

class Point {
    int x;
    int y;
    String label;

    void describe() {
        System.out.println(label + " is at (" + x + ", " + y + ")");
    }
}

class Leg {
    Point from;
    Point to;

    double length() {
        int dx = to.x - from.x;
        int dy = to.y - from.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

public class Route {
    public static void main(String[] args) {
        Point home = new Point();
        home.x = 0; home.y = 0; home.label = "Home";

        Point office = new Point();
        office.x = 3; office.y = 4; office.label = "Office";

        Leg commute = new Leg();
        commute.from = home;
        commute.to = office;

        home.describe();
        office.describe();
        System.out.println("leg length = " + commute.length());
    }
}
$ javac Route.java
$ ls *.class
Leg.class
Point.class
Route.class

$ java Route
Home is at (0, 0)
Office is at (3, 4)
leg length = 5.0

Ba class trong một file, ba file class. Leg cũng cho thấy một field có thể là reference tới một object thuộc class do bạn viết — commute.to.x chỉ là một chuỗi đọc field hết sức bình thường.

Public class phải trùng tên file

Quy tắc của bài 4 áp cho mọi class trong file, không riêng class chứa main. Nhiều nhất một class trong file được public, và tên của nó phải bằng tên file. Đánh dấu Point là public trong TwoPublic.java là hỏng trước khi chạy được dòng nào:

public class Point { int x; int y; }

public class TwoPublic {
    public static void main(String[] args) {
        System.out.println(new Point().x);
    }
}
TwoPublic.java:1: error: class Point is public, should be declared in a file named Point.java
public class Point { int x; int y; }
       ^
1 error

Dự án thật để một class một file và đánh dấu public, nên quy tắc này ít khi bị cấn. Tách ví dụ trên thành Point.javaApp.java chạy được ngay không cần thêm thao tác nào, vì javac tự compile file source được tham chiếu nếu nó nằm cạnh file bạn đưa vào:

$ ls
App.java  Point.java

$ javac App.java
$ ls *.class
App.class
Point.class

Object nằm trong array

Array của một class type là array của reference, nên các slot khởi đầu ở null và cứ thế cho tới khi bạn đặt object vào từng chỗ. Tạo array ra không tạo object Point nào cả:

Point[] route = new Point[3];
System.out.println("route.length   = " + route.length);
System.out.println("route[0]       = " + route[0]);

route[0] = new Point();
route[0].x = 0; route[0].y = 0; route[0].label = "Home";

route[1] = new Point();
route[1].x = 3; route[1].y = 4; route[1].label = "Office";

for (Point p : route) {
    if (p == null) {
        System.out.println("(empty slot)");
    } else {
        p.describe();
    }
}
route.length   = 3
route[0]       = null
Home is at (0, 0)
Office is at (3, 4)
(empty slot)

new Point[3] chạy một lần allocate array còn new Point() chạy hai lần, nên slot thứ ba vẫn giữ đúng null mà array sinh ra đã có. Duyệt một array object mà không kiểm tra null chính là cách NullPointerException ở trên đi thẳng ra production. Array là chủ đề của bài 15; điều duy nhất mới ở đây là kiểu phần tử là một class thì chẳng đổi gì ngoài chuyện mỗi slot đang giữ cái gì.

Cái gì nên nằm trong một class

Một class xứng đáng tồn tại khi một nhóm giá trị luôn đi cùng nhau và có hành vi chỉ có nghĩa trên đúng nhóm đó. x, ylabel là một điểm; chuyền ba variable rời rạc qua năm method rồi hy vọng chúng không lệch nhau là phiên bản không có class của cùng chương trình ấy.

Ba câu hỏi giải quyết được gần hết thiết kế của người mới:

Câu hỏiNguyên tắc
Field nào?Chỉ giữ state mà object cần để tự trả lời về chính nó. Cái gì suy ra được từ field khác, ví dụ một khoảng cách, nên là method thay vì một field có thể cũ đi lúc nào không hay.
Method nào?Hành vi đọc hoặc thay đổi state của chính object này. Một method không đụng tới field nào là dấu hiệu nó thuộc về chỗ khác.
Một class hay hai?Nếu một nửa số field luôn đi với nhau còn nửa kia không bao giờ chạm tới, đó là hai class. Leg ở trên giữ hai reference Point chứ không giữ bốn tọa độ rời.

Đặt tên theo đúng quy ước phần còn lại của Java đang dùng, và người review có bắt lỗi thật: PascalCase danh từ số ít cho class (Point, Invoice, HttpRequest, đừng đặt Points hay PointManager cho một class giá trị bình thường), camelCase danh từ cho field, camelCase động từ cho method (describe, length, distanceFromOrigin). Một class đặt tên theo object đó là gì gần như luôn sống thọ hơn một class đặt tên theo việc code hiện tại đang làm gì với nó.

Lỗi thường gặp và message chúng sinh ra

Gọi instance method từ static main mà không có object. Lỗi của bài 19, lần này gặp lại vì một lý do thật:

public class NoObject {
    int x = 3;

    void describe() {
        System.out.println("x is " + x);
    }

    public static void main(String[] args) {
        describe();
    }
}
NoObject.java:9: error: non-static method describe() cannot be referenced from a static context
        describe();
        ^
1 error

describe() đọc x, mà x thuộc về một object, còn main đang chạy khi chưa có object nào. Tạo một object rồi gọi qua nó: new NoObject().describe();.

Khai báo reference rồi quên new. Khai báo tạo ra variable chứ không tạo object, và compiler từ chối đọc nó:

Point p;
p.x = 3;
ForgotNew.java:6: error: variable p might not have been initialized
        p.x = 3;
        ^
1 error

Bỏ mất keyword new. Thiếu nó thì Point(...) bị hiểu là lời gọi một method tên Point:

Point p = Point();
MissingNew.java:5: error: cannot find symbol
        Point p = Point();
                  ^
  symbol:   method Point()
  location: class MissingNew
1 error

Truyền argument cho một constructor không tồn tại. Default constructor ngầm định không nhận argument nào, nên đoạn này còn hỏng cho tới khi bài 24 chỉ cách tự viết constructor:

Point p = new Point(3, 4);
CtorArgs.java:5: error: constructor Point in class Point cannot be applied to given types;
        Point p = new Point(3, 4);
                  ^
  required: no arguments
  found:    int,int
  reason: actual and formal argument lists differ in length
1 error

So sánh object bằng ==. Cái này compile được, chạy được, và lặng lẽ trả lời sai câu hỏi — xem lại phần equals ở trên, chỗ hai object Point có field giống hệt nhau mà so ra false.

Dùng một field object trước khi gán nó. Một field kiểu reference khởi đầu ở null, và đọc xuyên qua nó hỏng lúc chạy chứ không hỏng lúc compile, vì compiler không có cách nào biết bạn quên:

Leg commute = new Leg();
System.out.println("commute.from = " + commute.from);
System.out.println(commute.from.x);
commute.from = null
Exception in thread "main" java.lang.NullPointerException: Cannot read field "x" because "commute.from" is null
	at FieldNull.main(FieldNull.java:12)

new Leg() tạo ra một Leg và không tạo thêm gì nữa. Field fromto của nó giữ đúng null mà bước 2 của new đặt vào, còn tạo các object Point để lấp vào đó là một việc riêng. Constructor của bài 24 tồn tại phần lớn là để chuyện này không thể bị quên.

FAQ

Class và object trong Java khác nhau ở chỗ nào?

Class là một định nghĩa type: bản mô tả một object thuộc type đó giữ state gì và làm được gì. Object là một instance dựng ra từ bản mô tả đó bằng new, nằm trên heap với bản sao riêng của từng field. Một class sinh ra bao nhiêu object cũng được, mỗi object có giá trị field độc lập, còn code method thì tất cả dùng chung một bản.

Một file Java chứa nhiều class được không?

Được. javac ghi ra một file .class cho mỗi class, nên compile một file có ba class thì ra ba file class. Hạn chế là nhiều nhất một trong số đó được public, và nếu có một cái public thì tên file phải trùng đúng tên nó. Code production dù sao cũng để một public class một file, nên quy tắc này gần như không bao giờ lộ ra.

Vì sao in object ra lại thấy Point@15db9742?

Vì class của bạn không override toString(), nên bản kế thừa từ Object chạy. Nó in tên class, dấu @, rồi identity hash code ở hệ 16. Con số đó không phải địa chỉ bộ nhớ và không ổn định giữa các JVM. Thêm một toString() của riêng bạn thì println sẽ dùng nó.

Vì sao hai object có field giống hệt nhau lại không bằng nhau?

== so sánh reference, còn method equals kế thừa từ Object được định nghĩa là identity, nên cả hai đều đang hỏi "có phải cùng một object không" chứ không hỏi "nội dung có khớp không". Hai object dựng từ hai lời gọi new thì không bao giờ là cùng một object. Muốn so nội dung thì hoặc kiểm tra thẳng những field bạn quan tâm, hoặc override equals — kèm theo phải override cả hashCode, và đó không phải bài tập cho người mới.

Có bắt buộc phải viết constructor không?

Không. Nếu một class không khai báo constructor nào, compiler cấp một default constructor no-argument ngầm định, và đó là thứ mọi ví dụ trong bài này đã dùng. Bạn cần constructor riêng khi một object không được phép tồn tại ở trạng thái mới lấp một nửa — đúng cái vấn đề mà ví dụ Leg đã đâm vào.

Field không gán gì thì giá trị mặc định là bao nhiêu?

Là 0 với các type số, false với boolean, code point 0 với char, và null với mọi reference type kể cả array. new ghi các giá trị default đó trước khi bất kỳ constructor nào chạy, nên một field không bao giờ là rác chưa khởi tạo. Local variable thì ngược lại: nó không có default, và compiler từ chối mọi chương trình có khả năng đọc nó trước khi ghi.

Kết luận

Class mô tả một type; object là một instance của nó với bộ field riêng và không có dòng code riêng nào. new allocate, ghi zero vào các field, chạy một constructor rồi trả về một reference — và cái variable của bạn giữ chính là reference đó chứ không phải object. Mọi thứ còn lại trong bài đều chảy ra từ hai câu đó: aliasing, == so identity, null cùng message nó sinh ra, và instance method đọc đúng object nằm bên trái dấu chấm.

Thứ còn thiếu một cách cố ý là quyền kiểm soát cách một object ra đời. Hiện tại một Point đến tay bạn toàn số 0 rồi bạn vá lại từng field một, và đó chính xác là cách commute.from kết thúc ở null.

Bài tiếp theo trong series: field, method và constructor trong Java — instance field ở mức sâu hơn, viết constructor, overload constructor, và khởi tạo một object đầy đủ ngay tại thời điểm nó được tạo ra.

Bài viết liên quan

[Java Basics] Phạm vi biến trong Java: biến cục bộ, field và static

Scope của variable trong Java: local variable, parameter, instance field và static field, block scope, shadowing, definite assignment và lifetime, cùng mọi lỗi cannot find symbol chạy thật trên JDK 21.

[Java Basics] Custom Exception trong Java: tự viết exception type của bạn

Cách viết một custom exception class trong Java: chọn extends Exception hay extends RuntimeException và cái giá thật sự của mỗi lựa chọn, bốn constructor của Throwable, mang dữ liệu có cấu trúc bằng field, chain cause và đọc phần Caused by cùng dòng ... N more, translate exception tại ranh giới API, và khi nào IllegalArgumentException đã nói đủ rồi.

[Java Basics] Mảng một chiều trong Java: khai báo, khởi tạo và duyệt mảng

Hướng dẫn đầy đủ về mảng một chiều trong Java - mọi cách khai báo, giá trị mặc định, field length, index và ArrayIndexOutOfBoundsException, duyệt mảng bằng for và for-each, cái bẫy reference, copy thật với Arrays.copyOf và System.arraycopy, cùng Arrays.equals, mọi ví dụ đều compile và chạy trên JDK 21.

[Java Basics] Tính đa hình trong Java: overriding và overloading

Tính đa hình trong Java trên OpenJDK 21: quy tắc method overriding, @Override, dynamic dispatch chứng minh bằng javap, overriding và overloading, field hiding, static hiding, upcasting và downcasting, cùng cái bẫy trong constructor.