Ta đã biết, khi một đối tượng không còn tác dụng nữa, thì chúng sẽ bị GC dọn đi để tiết kiệm bộ nhớ, nhưng thế nào là “hết tác dụng” ?
Có thể tóm gọn bằng một câu cơ bản “Object tại heap sẽ không còn hữu dụng nếu chúng mất kết nối tới stack”
Object mất kết nối tới stack khi không còn một con trỏ nào chỉ tới chúng nữa cả, hãy xem thử đoạn code sau:
Object o = new Object();
System.out.println(o);
o = null;
Ở dòng đầu tiên, ta tạo một đối tượng o, o lúc này thực chất đang chỉ tới giá trị thực sự của đối tượng ta vừa tạo ra trong heap
Khi in thử đối tượng này ra, ta có output theo mẫu sau
java.lang.Object@4617c264
Tiếp theo, ta cập nhật o thành null. Lúc này, đối tượng nằm ở heap không còn cái gì trỏ đến nó nữa, và không gì có thể truy cập lại nó nữa, và lúc này nó trở thành mồi của GC
Vấn đề không đơn giản
Ví dụ trên khá đơn giản do chỉ có 1 object, vấn đề sẽ phức tạp hơn khi ta tiếp cận với nhiều object liên kết với nhau 1 lúc
Giả sử với 6 dòng lệnh sau, ta sẽ đi qua từng dòng một
Sau 4 dòng đầu tiên, stack và heap của chúng ta có dạng như sau. Tại stack là các biến giữ con trỏ tới các giá trị thực trong heap.
Sau dòng thứ 5, ta có hình như sau. Ta thấy, dù sau khi đã cập nhật p1 thành null với mong muốn GC sẽ dọn object này, nhưng thực tế, ta thấy ta vẫn có thể truy cập vào p1 bằng person.get(1) qua list persons, tức là ta vẫn có cách reach đến điểm này
Chỉ sau khi set cả list thành null, ta mới mất hoàn toàn liên hệ với p1
=> Không khó để đánh giá các phần tử nào sẽ được Garbage Collector dọn khi bạn đã hiểu về quan hệ bên trên. Dễ dàng thấy, sau 6 bước, p1 mất hoàn toàn liên hệ với stack và sẽ được dọn
Tuy nhiên, đó là ta nhìn thủ công, còn để garbage collector biết cái nào còn kết nối với stack sẽ tốn một lượng thời gian, và nó sẽ làm chậm hệ thống lại (chi tiết ở bên dưới). Có nhiều cách để làm điều này, và ta sẽ thảo luận ở bên dưới
Đánh dấu đệ quy
Ta sẽ thử đánh dấu các object còn live và object nào có thể bị dọn bởi GC. Dễ dàng, ta thêm 1 bit để dánh dấu xem chúng có còn kết nối với stack hay không. Khi tạo, ta sẽ để bit là 0, và khi ở giai đoạn đánh dấu, object là 0 sẽ bị xoá đi, còn nếu vẫn còn sử dụng, ta sẽ cập nhật nó thành 1
Tuy nhiên, heap và stack thay đổi liên tục. Cách đánh dấu được triển khai tuỳ thuộc vào phiên bản Java và GC bạn sử dụng, tuy nhiên, ta sẽ thử xem cách hệ thống đánh từ stack, hãy thử xem ví dụ sau
Đầu tiên toàn bộ phần tử sẽ được để là 0
Sau đó, toàn bộ các phần tử có kết nối trực tiếp tới stack được đánh thành 1
Tuy nhiên, do p1 vẫn có thể truy cập từ danh sách persons, và hơn nữa, ta không thể cứ dọn những gì vẫn còn kết nối. Vì vậy ta cần một cách duyệt đơn giản, với điểm khởi đầu từ các điểm đang = 1, sau đó đi đến mọi quan hệ của nó và đánh chúng thành 1, rồi tiếp tục đệ quy.
Các thuật toán đánh dấu đóng vai trò quan trọng trong giai đoạn đánh dấu, đầu tiên thử đi qua cách stop-the-world
Kĩ thuật Stop-The-World – Dừng thế giới
Với cách đánh dấu trên, ta nhận thấy. Nếu một phần tử được tạo trong quãng đánh dấu, nó sẽ không còn đúng nữa.
Vì vây, có một solution đơn giản là ta sẽ dừng mọi luồng khác và chỉ chạy luồng đánh dấu của cả chương trình, điều này sẽ rất ảnh hưởng đến hiệu năng. Ta sẽ thử xem các thuật toán tiếp theo.
Kĩ thuật Reference counting – Đếm liên hệ
Một cách triển khai khác là đếm số lần một object được trỏ đến. Mỗi object sẽ chứa số lần object được trỏ như một thông số mà nó nắm giữ. Như vậy, GC chỉ việc quét qua và xoá mọi object có 0 lần bị nắm giữ. Cách này sẽ không cần stop-the-world như cách đánh số đệ quy nữa, vì khi object được tạo ra giữa chừng lúc đánh số lần trỏ thì nó vẫn đều là 1 rồi.
Tuy nhiên, nó có một điểm yếu là sẽ tạo ra island of isolation, hay các vùng, một tập các object tự trỏ nhau nhưng mà thực tế không có kết nối tới stack
Giải phóng bộ nhớ
Cách thức làm sao để đánh dấu các object sẽ bị xoá sẽ được quyết định khác nhau bởi phiên bản Java và các kiểu GC khác nhau.
Giả sử ta đã đánh dấu được hết các object sẽ có thể bị xoá, tuy nhiên việc xoá chúng đi cũng không phải một quá trình đơn giản
Việc xoá object được gọi là sweeping by garbage collector, trong bài viết sẽ mention 3 cách thức sweeping khác nhau
Normal sweeping
Sweeping with compacting
Sweeping with copying
Normal sweeping
Hình ảnh trên thể hiện các khối bộ nhớ trong ram, các vùng có dấu X là các object đã được đánh dấu và chuẩn bị bị xoá
Sau khi xoá đi, vùng nhớ của ta có dạng như sau, dễ thấy, điều này dẫn tới một triệu chứng có tên Fragmentation
Fragmentation trong Java Garbage Collector
Lúc này, vì các vùng trống nằm ở giữa các vùng bị chiếm dụng, nên ta chỉ có thể thêm các bộ nhớ nhỏ hơn hoặc bằng vùng vào các vùng trống. Điều này sẽ dẫn tới 1 vài vấn đề khi ta muốn cấp phát một bộ nhớ lớn hơn
Giả dụ với trường hợp cấp phát một vùng nhớ lớn hơn các khe trống như ảnh trên, khi cấp phát vào, ta chỉ có thể xếp như sau:
Dù tổng thể bộ nhớ còn trống ta vẫn đủ để xếp vùng nhớ, nhưng thực tế thì ta không có một vùng nhớ liên tiếp nào chứa đủ vùng nhớ mới này. Và việc này sẽ throw ra 1 runtime exception là OutOfMemoryError.
Ưu nhược điểm của Normal sweeping
Normal Sweeping là một kĩ thuật giải phóng vùng nhớ ngây thơ, khá tiện dụng và đơn giản. Tuy nhiên sẽ dẫn tới các vùng nhớ bị phân mảnh. Quá trình này phù hợp khi ta có nhiều bộ nhớ, và ta chỉ cần nhanh chóng dọn bộ nhớ đi. Khi mà lượng vùng nhớ còn trống nhỏ hơn, ta sẽ prefer các kĩ thuật khác
Sweeping with compacting
Sweeping with compacting là một quá trình 2 bước. Đầu tiên, chúng vẫn giải phóng bộ nhớ, nhưng sau đó ta sẽ thực hiện thêm 1 bước gọi là compacting (thu gọn), ta sẽ dời toàn bộ vùng nhớ về phía đầu để đảm bảo không có bất kì khoảng trống nào ở giữa
Ưu nhược điểm Sweeping with compacting
Cách làm này giúp bộ nhớ không còn bị phân mảnh như thông thường, tuy nhiên việc di chuyển các vùng nhớ về đầu là một quá trình tốn kém, vì gần như với lượng vùng nhớ nhỏ và trải dài nhiều, ta sẽ phải copy và di chuyển khá nhiều trên vùng nhớ.
Sweeping with copying
Ở cách làm này, ta sẽ cần 2 vùng nhớ khác nhau. Ta sẽ không trực tiếp xoá các vùng nhớ bị đánh dấu là xoá đi, mà ta sẽ copy các vùng nhớ k bị xoá vào vùng nhớ mới
Sau đó mới thực hiện xoá toàn bộ vùng nhớ ở vùng nhớ cũ
Ưu nhược điểm của Sweeping with copying
Dữ liệu không bị phân mảnh
Về hiệu năng thì nhanh hơn compacting, do không phải thực hiện nhiều công đoạn tính toán khi di chuyển, mà chỉ copy nhanh chóng sang vùng mới đang trống hoàn toàn (ở compacting ta ví dụ di chuyển vùng 30-50 sang vùng 0-10, đầu tiên ta phải di 30-40, sau đó lại di vùng 10-20 đi, …. và khó khăn hơn nhiều)
Tuy nhiên cần nhiều bộ nhớ hơn vào cùng 1 thời điểm, cần lượng bộ nhớ trữ còn lại đủ nhiều để di chuyển.
Đóng gói là một trong những tính chất quan trọng của lập trình hướng đối tượng
Đóng gói giúp che dấu thông tin bên trong của một đối tượng, chỉ cho phép các đối tượng khác tương tác thông qua các phương thức được cung cấp
Đóng gói giúp giảm sự phức tạp của chương trình, giúp dễ dàng bảo trì và mở rộng chương trình
Ví dụ, trong một chiếc xe ô tô, ta có rất nhiều cấu phận và thông số. Nó có động cơ, vô lăng, bình xăng, đánh lửa, …
Nhưng ta thường không tương tác trực tiếp được vào chúng, mà thông qua các phương thức như bấm ga, bấm còi, đạp phanh, …
Khi bấm ga, có thể cả một quy trình rất dài đang xảy ra, như động cơ chạy, bình xăng bơm xăng, đánh lửa đốt xăng, …
Nhưng ta không cần quan tâm đến những thứ đó, ta chỉ cần bấm ga, và xe sẽ chạy
Ngoài ra, đóng gói giúp ta đảm bảo việc dữ liệu sẽ được giấu khỏi các đối tượng khác, và chỉ có các phương thức được cung cấp mới có thể tương tác với dữ liệu đó. Giúp đảm bảo tính toàn vẹn của dữ liệu.
Ví dụ:
getter, setter là một ví dụ về đóng gói. Nhờ getter, setter, ta có thể đảm bảo dữ liệu được cập nhật từ đối tượng khác sẽ luôn theo ý của mình, hay dữ liệu được lấy ra từ đối tượng khác sẽ luôn đúng theo ý của mình
Ví dụ trong các thư viện của Java, ta có thể thấy rất nhiều ví dụ về đóng gói. Ví dụ như trong class Scanner, ta có thể thấy rằng các biến được khai báo là private, và ta không thể truy cập trực tiếp vào chúng. Nhưng ta có thể sử dụng các phương thức như nextInt(), nextDouble(), nextLine(), … để lấy dữ liệu từ bàn phím. (Có thể ctrl + click vào các phương thức đó để xem code, sẽ thấy Scanner sử dụng các biến private để lấy dữ liệu từ bàn phím)
Đóng gói xuất hiện ở mọi nơi: Từ cả cái máy tính của bạn chỉ cần 1 nút bật, một game với cả trăm nghìn class chỉ cần 1 lần nhấn Play sẽ bắt đầu chạy, các thư viện Java với vô vàn chức năng đã được đóng gói và cho phép ta sử dụng qua những hàm rất đơn giản.
Trong lập trình phần mềm, yêu cầu của một chương trình thay đổi liên tục đáp ứng nhu cầu của khách hàng. Việc đóng gói và phân tách công việc của những bộ phận khác nhau là cực kì quan trọng, khi đó, ta sẽ cố gắng giới hạn những thay đổi chỉ bên trong 1 bộ phận nhỏ, độc lập nhất có thể và ít ảnh hưởng tới nhiều chỗ khác.
Kế thừa
Kế thừa là một trong những tính chất quan trọng của lập trình hướng đối tượng
Kế thừa giúp ta có thể sử dụng lại các đặc tính của một đối tượng khác, giúp giảm sự lặp lại của code, giúp dễ dàng bảo trì và mở rộng chương trình
Kế thừa giúp ta có thể tạo ra các đối tượng mới từ các đối tượng đã có, và có thể thêm các đặc tính mới vào các đối tượng mới đó
Ta nhận thấy kế thừa, hay sơ đồ cây xuất hiện ở mọi nơi trong cuộc sống. Như một chiếc ô tô có thể gọi là xe bốn bánh, xe bốn bánh có thể coi là phương tiện giao thông, …. Con người có thể coi là động vật. trong sinh học ta có các giống loài, trong vật lý có nhiều loại sóng khác nhau cùng là sóng,có chung tính chất là sóng, …
Ví dụ:
Ví dụ ta có nhiều class cần tạo như Chó, Mèo, Gà, Vịt, … Ta nhận thấy chúng đều có chung 1 đặc điểm là chúng đều là động vật, và có chung 1 số đặc điểm như là có thể ăn, có thể ngủ, có thể chạy, có thể bơi, … Ta có thể tạo ra một class cha là Động vật, và các class trên sẽ kế thừa từ class Động vật. Như vậy, ta có thể sử dụng lại các đặc điểm của class Động vật cho các class con, và có thể thêm các đặc điểm mới vào các class con đó, ví dụ Chó thì có thêm đặc điểm là sủa, Mèo thì có thêm đặc điểm là kêu meo meo, …
Các phương thức, thuộc tính của class cha sẽ được các class con kế thừa, và có thể sử dụng lại. Ngoài ra, các class con có thể thêm các phương thức, thuộc tính mới vào các class con đó. Điều này giúp giảm bớt các code dư thừa, giúp dễ dàng bảo trì và mở rộng chương trình
Có thể ở các class nhỏ hiện tại các bạn chưa thấy kế thừa hữu ích. Tuy nhiên khi bài toán ngày càng lớn lên, đặc biệt thiên về domain chuyên ngành, 1 class sẽ có rất nhiều thuộc tính và phương thức. Ví dụ: một cái bánh bình thường ở tạp hoá, có thể có lên tới 10-15 thuộc tính như màu sắc, hình dạng, giá, hạn sử dụng, ngày sản xuất, thành phần sản phẩm, các chứng chỉ an toàn thực phẩm, mã barcode, hay có các biến như “có thể cho động vật ăn”, … sau đó lại có nhiều loại bánh khác nhau nữa
Class Object
Trong Java, mọi class đều kế thừa từ class Object
Class Object là class cha của tất cả các class khác
Class Object có các phương thức như equals(), toString(), hashCode(), clone(), finalize(), wait(), notify(), notifyAll(), …
Các phương thức này được các class khác kế thừa, và có thể sử dụng lại
Có nhiều phương thức các bạn chưa cần đào sâu. Tuy nhiên tạm thời có thể để ý nó có các phương thức quan trọng như equals, toString, hashCode. Đây là các phương thức thường được dùng.
Đa hình
Đa hình là một trong những tính chất quan trọng của lập trình hướng đối tượng
Đa hình giúp ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một kiểu dữ liệu, giúp giảm sự lặp lại của code, giúp dễ dàng bảo trì và mở rộng chương trình
Override
Override là một kĩ thuật trong đa hình, giúp ta có thể thay đổi cách thức hoạt động của một phương thức đã có sẵn trong class cha
Override giúp ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một class cha, và khi gọi các phương thức, ta sẽ thấy mỗi đối tượng sẽ thực hiện theo cách của nó, chứ không phải cách của đối tượng khác
Overload là một kĩ thuật trong đa hình, giúp ta có thể tạo ra nhiều phương thức cùng tên, nhưng khác nhau về tham số truyền vào
Overload giúp ta có thể sử dụng các phương thức khác nhau, nhưng có cùng một tên, và khi gọi các phương thức, ta sẽ thấy mỗi phương thức sẽ thực hiện theo cách của nó, chứ không phải cách của phương thức khác
Cú pháp:
publicclassClassCha {
publicvoidmethodName(int a) {
// code
}
publicvoidmethodName(int a, int b) {
// code
}
}
Dễ thấy overload trong các phương thức của class Math, như Math.max(), Math.min(), Math.abs(), Math.pow(), Math.sqrt(), Math.round(), Math.floor(), Math.ceil(), Math.random(), … Chúng đều có cùng một tên là Math, nhưng khác nhau về tham số truyền vào. Ví dụ nếu truyền vào số nguyên, thì xử lý khác, truyền số thực thì xử lý khác, …
Phân biệt Overload và Override
Overload
Override
Cùng tên, khác tham số
Cùng tên, cùng tham số
Cùng class
Khác class
Overload giúp ta có thể tạo ra nhiều phương thức cùng tên, nhưng khác nhau về tham số truyền vào
Override là một kĩ thuật trong đa hình, giúp ta có thể thay đổi cách thức hoạt động của một phương thức đã có sẵn trong class cha
Overload chạy trong compile time
Override chạy trong run time
Đa hình compile time và runtime
Đa hình compile time là đa hình mà chúng ta có thể thấy được ngay trong quá trình code, ví dụ như overload. Đa hình compile time sẽ được thực hiện trong compile time, tức là khi chúng ta build chương trình, chúng ta sẽ thấy các phương thức được overload sẽ được thực hiện theo cách của nó, chứ không phải cách của phương thức khác
Đa hình runtime là đa hình mà chúng ta không thể thấy được ngay trong quá trình code, ví dụ như override. Đa hình runtime sẽ được thực hiện trong runtime, tức là khi chúng ta chạy chương trình, chúng ta sẽ thấy các phương thức được override sẽ được thực hiện theo cách của nó, chứ không phải cách của phương thức khác
Ví dụ: Khi viết code lúc compile ta đã thấy ngay phương thức nào được gọi, nhưng khi chạy chương trình, ta mới thấy phương thức nào được thực hiện của override
Ví dụ:
Ta có các class Chó, Mèo, Bò. Chúng đều kế thừa từ class Động vật. Ta có thể tạo ra một mảng các đối tượng Động vật, và có thể thêm vào đó các đối tượng Chó, Mèo, Bò. Như vậy, ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một kiểu dữ liệu, là Động vật.
Chúng có thể có chung 1 hàm là keu(), nhưng khi gọi hàm keu() trên mảng các đối tượng Động vật, ta sẽ thấy mỗi đối tượng sẽ keu() theo cách của nó, chứ không phải cách của đối tượng khác. Như vậy, ta có thể sử dụng các đối tượng khác nhau, nhưng có cùng một kiểu dữ liệu, và khi gọi các phương thức, ta sẽ thấy mỗi đối tượng sẽ thực hiện theo cách của nó, chứ không phải cách của đối tượng khác.
Tạo ra một class ConNguoi, có các thuộc tính là tên, tuổi, giới tính, …
Có các class con là SinhVien, NhanVien, HocSinhCap3 kế thừa từ class ConNguoi
Các class con có thể có thêm các thuộc tính riêng của nó, ví dụ SinhVien có thuộc tính là mã sinh viên, NhanVien có thuộc tính là mã nhân viên, …
Các class con có thể có thêm các phương thức riêng của nó, ví dụ SinhVien có phương thức là học, NhanVien có phương thức là làm việc, …
Có 1 phương thức chung của 3 class con là lamViecBuoiSang(), nhưng mỗi class con sẽ thực hiện theo cách của nó, chứ không phải cách của class con khác. Ví dụ SinhVien sẽ đi học đại học, NhanVien sẽ đi làm, HocSinhCap3 sẽ đi học ở trường cấp 3
Đôi khi, khi thực hiện các lệnh trong Javascript, bạn có thể gặp một lỗi như thế này
“Access to fetch at xxx from origin null has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
Mặc định thì, trình duyệt chỉ cho phép JavaScript gửi các HTTP request về phía server ở “cùng một nguồn – same origin” với chỗ mà script được load. Điều này được quy định bởi same-origin policy (SOP) – quy tắc 1 nguồn, đây là một tảng đá quan trọng trong bảo mật ứng dụng web.
Nguồn của 1 trang web là tổng hợp bởi protocol, địa chỉ máy chủ, cổng của url. Nếu không có cổng thì cổng mặc định sẽ được dùng (http thì là 80, https là 443). Ví dụ: url https://www.google.com/search có protocol là https, địa chỉ máy chủ là www.google.com và cổng port là 443. Hai url được gọi là cùng nguồn (same origin) nếu protocol, host và port đều giống nhau
Điều này giúp web được bảo mật hơn, vì các dữ liệu đều đảm bảo chỉ đến từ nguồn ta đang truy cập.
Vấn đề giả định
Thông thường, bất kì request nào của chúng ta đều chứa các cookie. Ở các website hiện đại, các cookie này thường chứa các thông tin bảo mật, ví dụ như thông tin tài khoản mật khẩu, … Ví dụ bạn vào 1 website nào đó, sau khi đăng nhập và lần sau vào lại trang bạn không cần đăng nhập nữa, vì trình duyệt đã nhớ thông tin đó ở đâu đó để không cần nhập lại nữa. (Điều này sẽ phức tạp hơn, nhưng sẽ không đi sâu ở đây).
Điều này dẫn đến 1 việc, khi bạn vào trang https://hacker.com chẳng hạn, trang này sẽ tự động gửi 1 request khác sang https://nganhang.com, request này lại đang chứa hết cookie, hay các thông tin của bạn. Và hacker.com có thể đọc trộm được thông tin của bạn khi request trả về.
ĐIều này có thể được sửa chữa bằng Same Origin Policy bên trên, hoặc SameSite=Strict trong cookie, bắt buộc 1 request khi gửi sang 1 server khác, thì chỉ đang chạy trên web đó. (tức là ở nganhang.com mới được gửi request gì chứa thông tin sang nganhang.com) để bảo mật
Tuy nhiên, giả sử bạn đang làm 1 hệ thống thông tin thời tiết ở web thoitiet.com, và bạn muốn bạn bè mình ở các trang như baochi.com, quochung.com có thể gọi tới lấy thông tin thì lại không được. Điều đó dẫn ta tới CORS
Cross-Origin Re-source Sharing (CORS)
Tuy nhiên, trang web chúng ta ngày càng mở rộng, ta không chỉ xuất dữ liệu từ 1 trang tĩnh duy nhất nữa. Có thể ta muốn gọi api để lấy dữ liệu từ một trang báo gì đó để hiển thị trên trang của mình, ta cần lấy dữ liệu về số ca mắc covid 19 từ đâu đó, ta cần lấy thông tin thời tiết, … Lúc này ta sẽ đi đến lí thuyết CORS.
Preflight requests
Hiểu đơn giản, CORS là một số quy định ta có thể cài đặt ở phía nhận dữ liệu, để thông báo là: tôi cho phép ông A, ông B lấy dữ liệu, vì ông này web nhà tôi, còn các ông C, D, E thì không
Lúc này, phía client sẽ gửi trước một “preflight request” để xem, liệu mình có quyền được truy cập dữ liệu hay không.
Định nghĩa: Preflight request là một request sẽ xảy ra do “bình thường” thì trình duyệt sẽ chặn các requets vi phạm SOP. Lúc này trình duyệt sẽ thực hiện 1 request OPTIONS tới server để xem request có được cho phép hay không. Server có thể deny hoặc allow request với một số limit về headers và method (ví dụ như method nào được dùng, header phải thế nào, …)
Phía Server sẽ phản hồi bằng cách gửi về 1 request với header chứa thông tin rằng các thông số cross-origin như nào thì sẽ được chấp nhận. Nếu như request ban đầu không match với các điều kiện từ phía server, hoặc server không phản hồi 1 CORS header nào, trình duyệt sẽ chặn và k gửi request.
CORS Header
CORS header
Response
Description
Access-Control-Allow-Origin
Both
Chỉ ra origin, hay nguồn nào được phép gửi request, để * để cho phép mọi nguồn
Access-Control-Allow-Headers
Preflight
Cho phép các header nào được chấp thuận ở request thật, để * để cho phép mọi header
Access-Control-Allow-Methods
Preflight
HTTP method nào được chấp thuận, để * để cho phép mọi method
Access-Control-Allow-Credentials
Both
Chỉ ra trình duyệt có được chứa thông tin tài khoản trong request không, thông tin có thể là basic authentication, browser cookie, hoặc chứng chỉ TLS phía client. Nếu là “true” thì các header khác không được để *
Access-Control-Max-Age
Preflight
Chỉ ra lượng thời gian (tính bằng giây) để trình duyệt cache phản hồi cors. Thường trình duyệt sẽ lưu khoảng 24 tiếng, chrome là 10 phút. Phần này chỉ áp dụng cho header và http method cho phép.
Both tức là header này có thể tồn tại ở các phản hồi từ server khi nhận request thực sự (actual) và cả request preflight. còn nếu là actual tức là header chỉ tồn tại ở phản hồi từ server khi nhận actual request (request thật), preflight nghĩa là header chỉ tồn tại ở phản hồi từ server khi nhận preflight request.
Cách Java lưu trữ dữ liệu (Kiểu dữ liệu nguyên thuỷ, Object, Wrapper class, Auto-boxing, Auto-unboxing)
Constructor, Super, This
Pass by value
Garbage Collector
I. Cách Java lưu trữ dữ liệu (Kiểu dữ liệu nguyên thuỷ, Object, Wrapper class, Auto-boxing, Auto-unboxing)
1. Kiểu dữ liệu nguyên thuỷ (Primitive data type)
Trong Java, có 8 kiểu dữ liệu nguyên thuỷ (Primitive data type): byte, short, int, long, float, double, boolean, char.
Kiểu dữ liệu nguyên thuỷ là các kiểu dữ liệu mà không phải là đối tượng, không có các phương thức, thuộc tính, … như các đối tượng.
Tức là, bạn không thấy các phương thức toString(), equals(), hashCode(), … trong các kiểu dữ liệu nguyên thuỷ.
inta=1;
// a không có hàm hay biến như các object// a.toString() không tồn tại// a.equals() không tồn tại// a.(gì đó) không tồn tại
Ví dụ, khi khai báo 1 biến int a = 1;, thì Java sẽ cấp phát 1 vùng nhớ có kích thước 4 byte để lưu giá trị 1 vào biến a. Thực chất trong bộ nhớ nó được lưu theo binary là 00000000 00000000 00000000 00000001. (1 byte = 8 bit)
Do có các 32 bit, nên int có thể lưu giá trị từ -2^31 đến 2^31 – 1. (1 bit để lưu dấu)
Tương tự, các kiểu dữ liệu khác cũng được lưu theo binary, và có kích thước khác nhau.
2. Object
2.1 Object là gì?
Trong Java, mọi thứ đều là đối tượng (Object). Vậy nên các kiểu dữ liệu nguyên thuỷ cũng là đối tượng.
Hiện tại các em chưa học về kế thừa, nhưng cứ tạm hiểu, khi ta nói về 1 tập đối tượng, ví dụ: Xe Ô Tô, hay class Car, có các thuộc tính, phương thức gì đó. Xe máy, hay class Motorbike, cũng có các thuộc tính, phương thức gì đó. Vậy thì Xe Ô Tô và Xe Máy đều là đối tượng, và chúng có các thuộc tính, phương thức giống nhau.
Vậy ta gọi tập đối tượng có cả xe máy và xe ô tô, không chứa các phương thức đặc thù như chạy, dừng, … là gì? Đó là tập đối tượng Xe, hay class Vehicle. Vậy nên, Xe Ô Tô và Xe Máy đều là Xe, hay Vehicle. Tập hợp Vehicle có thể nằm trong tập hợp to hơn là Phương Tiện Giao Thông, hay class Transportation.
Trong Java cũng như vậy, mọi class các em tạo ra mặc định đều thuộc 1 tập hợp chung là Object. Và class này luôn cung cấp cho mọi object tạo ra từ các class khác các phương thức, thuộc tính như toString(), equals(), hashCode(), … như đã nói ở trên.
2.2 Object được lưu thế nào trong Java
Hiểu đơn giản, giá trị cách lưu object thực sự rất phức tạp, điều đó đều do Java quản lý, và ta không cần quan tâm. Ta chỉ cần biết, khi tạo ra 1 object, ta chỉ lưu 1 địa chỉ tới object đó. Và từ địa chỉ này, ta có thể ra lệnh từ xa để lấy các thuộc tính, phương thức của object đó.
Vậy nên, khi ta gán 1 object cho 1 biến, thực chất ta đang gán 1 địa chỉ tới object đó cho biến đó.
Studentobj=newStudent();
// obj là 1 địa chỉ tới object Student
Vậy nên ta không thể so sánh 2 object với nhau bằng toán tử ==, mà phải dùng phương thức equals().
Wrapper class là các class được tạo ra để bao bọc các kiểu dữ liệu nguyên thuỷ. Ví dụ: Integer, Float, Double, Boolean, Character, …
Wrapper class cung cấp các phương thức để thao tác với các kiểu dữ liệu nguyên thuỷ. Ví dụ: Integer.parseInt(), Integer.toString(), Integer.valueOf(), …
Primitive data type
Wrapper class
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
boolean
Boolean
char
Character
Các kiểu dữ liệu lưu dạng object này tuân theo cách Java lưu trữ object, tức là không lưu trực tiếp object, mà chỉ lưu địa chỉ tới object đó.
Ưu điểm
Nhược điểm
Có thể sử dụng các phương thức của object
Tốn bộ nhớ hơn
Ví dụ, ở Java ta có thể chuyển String thành số, từ các số hay từ 1 object bất kì sang String rất nhanh bằng toString(), ngoài ra khi nó là Object, ta có thể sử dụng các kĩ thuật OOP như kế thừa, đa hình, … (sẽ nói ở các buổi sau).
inta=1;
// a không có hàm hay biến như các object// a.toString() không tồn tạiIntegerb=1;
// b có hàm toString()// b.toString() tồn tại
2.4 Auto-boxing và Auto-unboxing
Auto-boxing là quá trình chuyển đổi từ kiểu dữ liệu nguyên thuỷ sang kiểu dữ liệu lưu dạng object.
Auto-unboxing là quá trình chuyển đổi từ kiểu dữ liệu lưu dạng object sang kiểu dữ liệu nguyên thuỷ.
Autoboxing và Auto-unboxing giúp cho việc sử dụng các kiểu dữ liệu nguyên thuỷ và object trở nên dễ dàng hơn. Ví dụ, khi ta muốn sử dụng HashMap, thì ta không thể sử dụng kiểu dữ liệu nguyên thuỷ là int, mà phải sử dụng Integer.
Tuy nhiên, trong nhiều trường hợp nên hạn chế dùng các kiểu Object do chúng tốn bộ nhớ
for (Integeri=0; i < 1000000; i++) {
// ...
}
// Tốn bộ nhớ hơn// Vì i là object, nên phải tạo ra 1 object mới mỗi lần tăng i// Còn nếu dùng int, thì chỉ cần tạo ra 1 biến int i, và tăng i mỗi lần
3. Các phương thức khởi tạo (Constructor) trong Java
Constructor là một phương thức đặc biệt, nó được gọi khi ta khởi tạo một đối tượng. Constructor có tên giống với tên class, và không có kiểu trả về.
Constructor được sử dụng để khởi tạo các giá trị ban đầu cho đối tượng. Ví dụ như ta có một class Student, trong đó có các thuộc tính name, age, address, math, literature, english. Ta có thể tạo một constructor như sau:
Constructor có thể có hoặc không có tham số. Nếu không có tham số, thì constructor đó được gọi là constructor mặc định. Nếu có tham số, thì constructor đó được gọi là constructor có tham số.
Constructor có thể có nhiều tham số, và các tham số đó có thể là bất kỳ kiểu dữ liệu nào.
Constructor có thể có nhiều hơn một, và các constructor đó có thể có số lượng tham số khác nhau.
Constructor có thể gọi constructor khác của cùng một class bằng từ khóa this(). Ví dụ:
Trong quá trình thực thi chương trình, Java sẽ liên tục tìm các Object không được tham chiếu tới Heap Memory (mang giá trị null hoặc một vài trường hợp khác). Sau đó Garbage Collector sẽ tự động giải phóng bộ nhớ cho các ô nhớ chứa tham chiếu đó.
Điều này kiến lập trình viên không cần phải giải phóng bộ nhớ thủ công như C/C++. Thay vào đó, để giải phóng bộ nhớ, lập trình viên chỉ cần loại bỏ tham chiếu của Object.
5. Cách Java truyền tham số | Pass-by-value
Cách Java truyền tham số
5.1. Pass-by-value
Trong Java, khi ta truyền tham số vào một hàm, thì tham số đó sẽ được copy ra một vùng nhớ khác, và hàm sẽ thao tác với tham số ở vùng nhớ mới này.
Vậy nên, khi ta thay đổi giá trị của tham số trong hàm, thì giá trị của tham số bên ngoài hàm không bị thay đổi.
5.2. Tại sao pass-by-value mà String lại thay đổi được?
Ta hãy quay trở lại khái niệm vừa nhắc tới Khi ta truyền một biến nguyên thuỷ, hay trong trường hợp trên là age, ta đang như tạo một bản sao của một tờ giấy bình thường, tờ giấy đó ghi số là 20, ta đưa cho người khác. Họ sửa chúng thành 90, 100 hay gì đi chăng nữa, thì tờ giấy ban đầu của ta vẫn là 20
Tuy nhiên, khi ta truyền một biến reference (tham chiếu) của Object person, ta đang truyền bản sao của “tham chiếu” của nó. Hay đơn giản là, ta đang tạo ra bản sao của một cái điều khiển, vậy thì cái điều khiển bản sao này khi đưa cho người khác, vẫn sẽ bật tắt được tivi ban đầu của mình.
Class và Object (Lớp và Đối tượng) là hai trong những khái niệm quan trọng nhất của ngôn ngữ lập trình hướng đối tượng (OOP).
Sự khác biệt chính giữa một Class và một Object trong Java là:
Class là một mô hình chi tiết để bạn sử dụng tạo ra các Object. Class định nghĩa tất cả các thuộc tính và các phương thức cần thiết của một Object.
Mỗi Object phải thuộc một Class nào đó. Và một Object là một thể hiện của Class. Tất cả các Object thuộc về cùng một Class có cùng các thuộc tính và các phương thức.
Syntac :
ClassAobj=newClassA();
Vậy thực chất Java đã làm những gì?
Khi ta sử dụng new Example(), Java sẽ dành ra một số lượng vừa đủ ô nhớ trong bộ nhớ để lưu trữ các giá trị của một đối tượng Example và sau đó khởi tạo đối tượng đó.
Sau khi khởi tạo, Java sẽ lấy địa chỉ của vùng nhớ đó, gán vào cho “biến” obj. Biến obj được định nghĩa là kiểu Example, hay tức là nếu ta truy cập vào địa chỉ mà biến obj lưu thì vùng nhớ đó sẽ là vùng nhớ của một đối tượng Example.
Điều cần nói
Hiểu đơn giản, một object trong Java thực chất như 1 cái điều khiển, đang được cài đặt cho 1 cái tivi thực chất ở trong đó. Khi ta bấm nút trên điều khiển, thực chất ta đang gửi tín hiệu đến tivi, và tivi sẽ thực hiện hành động tương ứng. Vậy nên, khi ta gọi hàm obj.printInfo(), thực chất ta đang gửi tín hiệu đến đối tượng obj, và đối tượng obj sẽ thực hiện hành động tương ứng.
Lúc này, cứ tạm nhớ nó chỉ là cái điều khiển, chứ không phải cái tivi thật sự, sâu hơn ta sẽ tìm hiểu tiếp ở dưới. (Phần equal và hashcode)
Từ khóa this
Từ khoá this trong Java được sử dụng để tham chiếu đến đối tượng hiện tại. Từ khoá this có thể được sử dụng để tham chiếu bất kỳ biến nào của đối tượng hiện tại.
Trong ví dụ trên, ta có một class Person, trong đó có thuộc tính name. Ta muốn gán giá trị cho thuộc tính name, ta sẽ tạo một hàm khởi tạo, và trong hàm khởi tạo đó, ta sẽ gán giá trị cho thuộc tính name. Vì tên của tham số trùng với tên của thuộc tính, nên ta sẽ dùng từ khóa this để tham chiếu đến thuộc tính name.
Từ khóa this cũng có thể được sử dụng để tham chiếu đến các phương thức của đối tượng hiện tại.
Access modifier là một từ khóa trong Java, nó được dùng để chỉ định quyền truy cập của một thuộc tính hoặc một phương thức. Có 4 loại access modifier trong Java, đó là public, private, protected, default.
Bài này chỉ cần nói default, private, public
Public
Khi ta dùng từ khóa public với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó có thể được truy cập từ bên ngoài class.
Khi ta dùng từ khóa protected với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó chỉ có thể được truy cập từ bên trong class, hoặc từ bên trong các class con của class đó.
Khi ta không dùng từ khóa nào với một thuộc tính hoặc một phương thức, thì thuộc tính hoặc phương thức đó chỉ có thể được truy cập từ bên trong class, hoặc từ bên trong các class cùng package với class đó.
Khi ta dùng access modifier, ta có thể kiểm soát được quyền truy cập của các thuộc tính và phương thức. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Ta muốn thuộc tính name này chỉ có thể được truy cập từ bên trong class Person, và không thể được truy cập từ bên ngoài class Person. Vì vậy, ta sẽ dùng từ khóa private với thuộc tính name.
publicclassPerson {
private String name;
}
Điều cần nói
Trong thực tế, ví dụ như 1 cái ô tô, bên trong có rất nhiều cấu kiện tinh vi. Khi ta khởi động xe, các cấu kiện hoạt động với nhau, ta không biết chúng làm gì, làm như nào, chỉ biết là khi bật xe, thì xe chạy.
Các cấu kiện đó là những thứ “private”, chỉ bên trong một class “Oto” với nhau mới biết, bên ngoài không biết. Điều này giúp cho việc sử dụng xe dễ dàng hơn, thứ bên ngoài không cần biết quá nhiều về xe bên trong.
Getter và Setter
Getter
Getter là một hàm dùng để lấy giá trị của một thuộc tính. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Để lấy giá trị của thuộc tính name, ta sẽ tạo một hàm getName().
Setter là một hàm dùng để gán giá trị cho một thuộc tính. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Để gán giá trị cho thuộc tính name, ta sẽ tạo một hàm setName().
Khi ta dùng Getter và Setter, ta có thể kiểm soát được quyền truy cập của các thuộc tính. Ví dụ, ta có một class Person, trong đó có thuộc tính name. Ta muốn thuộc tính name này chỉ có thể được truy cập từ bên trong class Person, và không thể được truy cập từ bên ngoài class Person. Vì vậy, ta sẽ dùng từ khóa private với thuộc tính name, và tạo một hàm getName() và setName() để lấy và gán giá trị cho thuộc tính name.
Hiểu đơn giản hơn, khi để tất cả thuộc tính private và chỉ getter setter public, vô hình chung ta sẽ dẫn mọi truy cập vào 1 class chỉ còn 1 con đường, đó là getter và setter
Điều này giúp ta có được “quyền kiểm soát”, “control” ở 1 class đó, ta có thể kiểm soát được các giá trị được gán vào thuộc tính, và các giá trị được lấy ra từ thuộc tính.
Ví dụ, ta có một class là “NhaHang”, nó có một phương thức lấy số điện thoại, tuy nhiên ta muốn khi lấy số điện thoại, ta sẽ chuẩn hoá lại giá trị đó, ví dụ như thêm dấu +84 vào đầu, hoặc thêm dấu – vào giữa, hoặc thêm dấu cách vào giữa, hoặc thêm dấu ngoặc vào đầu cuối, … Vậy nên, ta sẽ tạo một hàm getter, và trong hàm getter đó, ta sẽ chuẩn hoá lại giá trị trước khi trả về.
class NhaHang {
private String soDienThoai;
String getSoDienThoai() {
// Chuẩn hoá lại số điện thoạireturn soDienThoai;
}
void setSoDienThoai(String soDienThoai) {
this.soDienThoai = soDienThoai;
}
}
Như này, ta đảm bảo số điện thoại được lấy ra dùng cho class khác luôn là số điện thoại chuẩn hoá, không cần phải chuẩn hoá lại nữa. Vì cách duy nhất để lấy số điện thoại ra là dùng hàm getter, và hàm getter đã chuẩn hoá rồi.
Tương tự với setter, giả sử ta có một class ConNguoi với thuộc tính chiều cao. Chiều cao thì không thể âm được, vậy nên ta sẽ tạo một hàm setter, trong đó ta sẽ kiểm tra giá trị trước khi gán vào thuộc tính.
class ConNguoi {
private int chieuCao;
void setChieuCao(int chieuCao) {
if (chieuCao < 0) {
this.chieuCao = 0;
} else {
this.chieuCao = chieuCao;
}
}
}
Điều này đảm bảo tính toàn vẹn của dữ liệu, giúp cho mọi thứ chính xác và chặt chẽ
Ngoài ra, nó cũng giúp tăng tính bảo mật, không lộ quá nhiều thông tin bên trong class ra bên ngoài, mà mọi thứ chỉ có thể truy cập, sử dụng qua các phương thức ta đã định nghĩa sẵn. (Tính chất này còn được gọi là encapsulation, đóng gói trong OOP)
Từ khoá Static
Static là gì
Static là một từ khóa trong Java, nó có thể được dùng với biến, hàm, class. Static có nghĩa là tĩnh, tức là nó chỉ tồn tại ở một vị trí duy nhất, tồn tại ngay cả khi chưa tạo ra đối tượng. Vì vậy, khi ta dùng static với biến, hàm, class, ta có thể gọi chúng mà không cần tạo ra đối tượng.
Ví dụ, trong các thư viện Java, ta thường thấy các hàm, biến, class static. Vì vậy, ta có thể gọi chúng mà không cần tạo ra đối tượng.
Math.sqrt(2);
Từ khoá static với biến
Khi ta dùng static với biến, thì biến đó sẽ được tạo ra ngay cả khi chưa tạo ra đối tượng. Vì vậy, ta có thể gọi biến đó mà không cần tạo ra đối tượng. Biến static này là tồn tại duy nhất, tức là nó chỉ có một giá trị duy nhất, và nó sẽ được sử dụng chung cho tất cả các đối tượng.
class Person {
static int count=0;
Person() {
count++;
}
}
Hàm main static
Hàm main() là một hàm static, nó được gọi khi chương trình bắt đầu chạy. Ví dụ:
public classHelloWorld {
public staticvoidmain(String[] args) {
System.out.println("Hello World");
}
}
Lí do hàm main là static
Như ta biết, để gọi 1 hàm từ 1 đối tượng, ta phải tạo ra 1 đối tượng đó trước.
Vậy nếu hàm main không phải là static, thì ta phải tạo ra 1 đối tượng của class HelloWorld trước, rồi gọi hàm main từ đối tượng đó.
Vì vậy, hàm main static thường xuất hiện ở class khởi nguồn. Như vậy, Java có thể gọi thẳng hàm main luôn mà không cần khởi tạo class trước đó.
Lí do hàm static chỉ có thể gọi hàm static khác
Tuy nhiên, 1 hàm static chỉ có thể gọi các hàm static khác, và chỉ có thể truy cập các biến static khác. Lí do là vì, khi ta gọi hàm static, ta không cần tạo ra 1 đối tượng, vậy nên ta không thể truy cập các biến không phải static của đối tượng đó được. Và vì ta không tạo ra 1 đối tượng, nên ta không thể gọi các hàm không phải static của đối tượng đó được.
Cứ tưởng tượng đơn giản, các hàm, biến, class static luôn chỉ tồn tại độc nhất, tồn tại ngay cả khi chưa tạo ra đối tượng. Vì vậy, các hàm, biến, class static luôn có thể được gọi mà không cần tạo ra đối tượng. Nếu mà 1 hàm static gọi 1 hàm không static, thì lỡ đâu hàm không static đó lại cần phải truy cập đến các biến không static, thì làm sao mà truy cập được, vì chưa tạo ra đối tượng mà.
Về equal và hashcode, ==
Trong Java, có 2 cách để so sánh 2 đối tượng với nhau, đó là so sánh bằng toán tử == và so sánh bằng hàm equal(). Tuy nhiên, 2 cách này lại có 2 cách hoạt động khác nhau.
Khi ta so sánh 2 đối tượng bằng toán tử ==, ta đang so sánh 2 địa chỉ của 2 đối tượng đó. Nếu 2 đối tượng đó có cùng địa chỉ, thì toán tử == sẽ trả về true, ngược lại thì trả về false.
Khi ta so sánh 2 đối tượng bằng hàm equal(), ta đang so sánh 2 nội dung của 2 đối tượng đó. Nếu 2 đối tượng đó có cùng nội dung, thì hàm equal() sẽ trả về true, ngược lại thì trả về false.
equal() dùng để so sánh nội dung của 2 đối tượng. (Lưu ý: toán tử == sẽ là so sánh giá trị của 2 biến đó, hay chính xác hơn là so sánh địa chỉ, chứ không phải so sánh nội dung)
hashcode() dùng để xác định vị trí của đối tượng đó trong một bảng băm (cấu trúc dữ liệu mà đa số sẽ sử dụng)
Vì như đã dạy ở trên, ta khi tạo 1 biến, thì thực chất nó chỉ là 1 tham chiếu
Vì vậy, các String có cùng 1 giá trị, dùng == để so sánh sẽ trả về false, vì chúng có các tham chiếu khác nhau
– Ta có thể thấy, khi gán 1 String khác, nếu không dùng new gán vào string cũ, chúng sẽ bằng nhau vì chúng cùng tham chiếu vào 1 địa chỉ
int a=5;
int b=5;
Integer a1=5;
Integer b1=5;
Integer a2=100;
Integer b2=100;
Integer a3=500;
Integer b3=500;
System.out.println(a == b); // true
System.out.println(a1 == b1); // true vì đây là tham chiếu, nhưng mà Java có cơ chế cache Integer từ -128 đến 127, nên khi gán 2 số từ -128 đến 127, chúng sẽ cùng tham chiếu đến 1 địa chỉ, nên == sẽ trả về true
System.out.println(a2 == b2); // true vì đây là tham chiếu, nhưng mà Java có cơ chế cache Integer từ -128 đến 127, nên khi gán 2 số từ -128 đến 127, chúng sẽ cùng tham chiếu đến 1 địa chỉ, nên == sẽ trả về true
System.out.println(a3 == b3); // false
System.out.println(a3.equal(b3)); // true vì đây là so sánh nội dung, không phải so sánh tham chiếu
String và String Builder trong Java
String là một class trong Java, nó được sử dụng để lưu trữ một chuỗi các ký tự. Một đối tượng String được tạo ra bằng cách sử dụng từ khóa new và có thể được khởi tạo bằng một chuỗi ký tự hoặc một đối tượng String khác.
Lí do là vì, khi ta tạo biến String mới, ta chỉ đang tạo 1 tham chiếu đến vùng nhớ của String đó.
String là một dạng immutable, tức là nó không thể thay đổi được. Vì vậy, khi ta thay đổi giá trị của String, ta đang tạo ra một String mới, và tham chiếu đến nó.
String str="Hello";
str = "World";
Để nhập vào String từ bàn phím, ta sử dụng hàm nextLine() của Scanner
String Builder là một class trong Java, nó được sử dụng để lưu trữ một chuỗi các ký tự. Một đối tượng String Builder được tạo ra bằng cách sử dụng từ khóa new và có thể được khởi tạo bằng một chuỗi ký tự hoặc một đối tượng String khác.
String Builder là một dạng mutable, tức là nó có thể thay đổi được. Vì vậy, khi ta thay đổi giá trị của String Builder, ta không tạo ra một String Builder mới, mà chỉ thay đổi giá trị của String Builder đó.
Để nhập vào String Builder từ bàn phím, ta sử dụng hàm nextLine() của Scanner như String
Vậy StringBuilder khác String ở chỗ nào
Stringstr="Hello";
str = str + " World";
Khi ta thực hiện phép cộng chuỗi, ta đang tạo ra một String mới, và tham chiếu đến nó. Vậy nên khi ta thực hiện phép cộng chuỗi nhiều lần, ta sẽ tạo ra rất nhiều String mới, và tham chiếu đến chúng. Điều này sẽ làm tốn rất nhiều bộ nhớ, và làm chậm chương trình.
Vì vậy, khi ta cần thay đổi giá trị của chuỗi nhiều lần, ta nên sử dụng StringBuilder, để tránh tạo ra quá nhiều String mới.
Bảng so sánh
String
StringBuilder
String là immutable
StringBuilder là mutable
Khi thay đổi ít lần, String tốn ít bộ nhớ hơn
Khi thay đổi ít lần, StringBuilder không được hết khả năng và tốn bộ nhớ nhiều hơn (lí do bên dưới)
Khi thay đổi nhiều lần, String tốn nhiều bộ nhớ hơn và thời gian hơn
Khi thay đổi nhiều lần, StringBuilder tốn ít bộ nhớ hơn và thời gian hơn
inttotal=50000;
Strings="";
for (inti=0; i < total; i++) { s += String.valueOf(i); }
// 4828msStringBuildersb=newStringBuilder();
for (inti=0; i < total; i++) { sb.append(String.valueOf(i)); }
// 4ms
Lí do StringBuilder có thể động như vậy vì nó sử dụng cơ chế mảng động, tức là khi khai báo 1 String độ dài 10, thực chất Java dành ra nhiều ô nhớ hơn để khi thêm độ dài mới vào tốc độ sẽ nhanh hơn (do không phải di chuyển toàn bộ vùng nhớ sang phần khác). Trong khi đó, String thực chất là một mảng tĩnh, khi khai báo 1 String độ dài 10, Java chỉ dành ra đúng 10 ô nhớ, khi thêm độ dài mới vào, Java sẽ phải di chuyển toàn bộ vùng nhớ sang phần khác, điều này sẽ làm chậm tốc độ xử lí.
Tuy nhiên, do dùng ô nhớ ít hơn, String tốn ít bộ nhớ hơn StringBuilder. Vì vậy, khi ta cần thay đổi giá trị của chuỗi ít lần, ta nên sử dụng String, để tránh tốn bộ nhớ.
Kĩ thuật mảng động: Nhân đôi mảng
Hiểu đơn giản, mảng luôn được khởi tạo số lượng phần từ nhiều hơn thực tế, để khi thêm phần tử vào mảng, ta không cần phải di chuyển toàn bộ mảng sang phần khác.
Điều này thực chất đang hi sinh bộ nhớ nhiều hơn để có hiệu quả thời gian tốt hơn trong đa số trường hợp.
7. Inner class
Inner class là gì
Inner class là một class được khai báo bên trong một class khác. Inner class có thể được khai báo là static hoặc non-static. Inner class có thể truy cập tất cả các biến và phương thức của class bên ngoài nó.
Inner class static là một class được khai báo bên trong một class khác, và được khai báo là static. Inner class static có thể truy cập tất cả các biến và phương thức của class bên ngoài nó.
Inner class non-static là một class được khai báo bên trong một class khác, và không được khai báo là static. Inner class non-static có thể truy cập tất cả các biến và phương thức của class bên ngoài nó.
Inner class có thể được dùng để tạo ra một class chỉ được sử dụng bởi một class khác. Ví dụ, ta có một class Person, trong đó có một class Address. Class Address này chỉ được sử dụng bởi class Person, nên ta có thể tạo ra một inner class Address.
từ việc import Scanner, import ArrayList, import String, … có thể import bất kì class nào trong Java, và sử dụng nó trong class của mình.
import java.util.Scanner;
classPerson {
String name;
int age;
voidinput() {
Scannersc=newScanner(System.in);
name = sc.nextLine();
age = sc.nextInt();
}
}
Tuy nhiên, có thể tự tạo ra các class của riêng mình, và sử dụng nó trong class của mình. Ví dụ có các Class như QuanLySinhVien, SinhVien, MonHoc, … thì có thể tạo ra các file QuanLySinhVien.java, SinhVien.java, MonHoc.java, … và sử dụng chúng trong class của mình.
import java.util.Scanner;
classSinhVien {
String name;
int age;
voidinput() {
Scannersc=newScanner(System.in);
name = sc.nextLine();
age = sc.nextInt();
}
}
classQuanLySinhVien {
SinhVien[] danhSachSinhVien;
voidinput() {
Scannersc=newScanner(System.in);
intn= sc.nextInt();
danhSachSinhVien = newSinhVien[n];
for (inti=0; i < n; i++) {
danhSachSinhVien[i] = newSinhVien();
danhSachSinhVien[i].input();
}
}
}
Lưu ý, trong 1 file thì chỉ có thể có 1 class public, và tên của class đó phải trùng với tên của file. Ví dụ, trong file QuanLySinhVien.java, ta chỉ có thể có 1 class public, và tên của class đó phải là QuanLySinhVien.
// QuanLySinhVien.java
public classQuanLySinhVien {
...
}
Ngôn ngữ lập trình Java được thiết kế để trở thành một ngôn ngữ không phụ thuộc vào nền tảng (machine-independent). Java có thể chạy trên bất kỳ nền tảng nào miễn là có máy ảo Java (Java Virtual Machine – JVM). Máy ảo Java là một chương trình có thể chạy trên nhiều nền tảng khác nhau mà không cần phải biên dịch lại. Máy ảo Java có thể chạy trên các máy tính, điện thoại, máy tính bảng, máy chủ, … Máy ảo Java có thể được cài đặt trên các hệ điều hành khác nhau như Windows, Linux, Mac OS, …
Java vừa đủ mạnh với nhiều thư viện, tính năng, bảo đảm các sự chặt chẽ, nhưng cũng đồng thời chạy rất nhanh. Java có thể được sử dụng để phát triển các ứng dụng desktop, web, mobile, game, … Java cũng là một trong những ngôn ngữ lập trình được sử dụng nhiều nhất hiện nay.
Điều cần nói
Q: Tại sao Java ra đời, tại sao cần hướng đối tượng chứ không code hết vào 1 file
A: Trước khi Java ra đời, các ngôn ngữ lập trình khác như C/C++ đã ra đời. Tuy nhiên, các ngôn ngữ này có một số hạn chế như sau:
Các ngôn ngữ này không thể chạy trên nhiều nền tảng khác nhau, mà phải biên dịch lại cho từng nền tảng khác nhau. Ví dụ như C++ trên Windows và C++ trên Linux là 2 ngôn ngữ khác nhau, nên phải biên dịch lại.
Các ngôn ngữ này không có khái niệm về hướng đối tượng, nên không thể phát triển các ứng dụng lớn, phức tạp.
Cơ chế hướng đối tượng của Java giúp chương trình trở thành các đối tượng làm việc với nhau, giúp cho việc phát triển các ứng dụng lớn, phức tạp trở nên dễ dàng hơn. (Sẽ nói sâu hơn vào phần sau)
Ba thành phần nền tảng Java không thể thiếu và cách chúng hoạt động cùng nhau trong các ứng dụng Java của bạn. Cụ thể:
JDK (Java Development Kit – Bộ công cụ phát triển Java)
JRE (Java Runtime Environment – Môi trường thực thi Java)
JVM (Java Virtual Machine – Máy ảo Java)
Cách Java thực hiện một đoạn code như sau:
Với các file code ví dụ là Party.java, nó sẽ được biên dịch bởi trình biên dịch javac trong JDK để tạo ra file Party.class chứa các bytecode.
Các file .class và các thư viện cần có cho Java sẽ được tổng hợp bởi JRE
Từ các đoạn mã máy trên, JRE sẽ đưa cho JVM để thực thi, và chạy một “máy ảo” độc lập để thực thi chương trình của chúng ta.
Điều đó nghĩa là, chương trình chúng ta đang chạy “tách biệt” với hệ thống (chạy trong JVM). Vậy nên với Java ta chỉ cần viết một lần, biên dịch một lần và nó có thể chạy ở mọi nền tảng mà JVM hỗ trợ. Khả năng đó được lý giải vì khác với các ngôn ngữ như C hay C++, Java được thiết kế theo nhiều lớp khác nhau để tách biệt giữa chương trình Java và hệ thống.
Java Virtual Machine (JVM)
Không như C/C++ khi mà code được biên dịch thì sẽ tạo thành các mã lệnh được làm cho riêng các vi xử lý khác nhau. Code java đầu tiên được biên dịch thành một dạng tổng quát – bytecode, là ngôn ngữ cho JVM chạy. Sau đó JVM mới chạy thành các ngôn ngữ máy cho nền tảng đó.
Trình tự hoạt động:
Bạn tạo ra một đoạn code (source) với đuôi .java
javac compiler biến nó thành 1 file .class
File .class sẽ được đọc bởi JVM, và chuyển thành bytecode để chạy trên các nền tảng khác nhau.
Điều cần nói
Tóm lại, Java sẽ không compile code ra thẳng mã máy, mà sẽ compile ra dạng .class đặc biệt, gọi là bytecode. Sau đó, JVM sẽ đọc bytecode này, và chuyển nó thành mã máy để chạy trên nền tảng đó.
Ưu điểm:
Khả năng độc lập với nền tảng, chỉ cần có JVM là được. Người dùng cuối, thiết bị cuối không cần cài cả bộ mingw như C++, họ chỉ cần JVM (là một phần nhỏ trong bộ JDK để lập trình), tức là bộ chỉ dùng để chạy thôi, và bộ thư viện này có sẵn trên hầu hết các thiết bị hiện nay.
Do chạy trên máy ảo, nên có tính bảo mật cao hơn, do mỗi chương trình là một máy ảo khác nhau
Nhược điểm:
Chậm hơn C++, vì phải chạy trên máy ảo. Code không được ra thẳng mã máy để chạy cho nhanh, và phải qua 1 tầng nữa để JVM biên dịch thành mã máy. Ví dụ: các thiết bị Android theo cơ chế này nên chúng thường mở ứng dụng chậm hơn IOS, và dùng tài nguyên nhiều hơn. Tuy nhiên những lần mở sau thì nhanh hơn, vì JVM đã biên dịch thành mã máy rồi, nên chỉ cần chạy mã máy thôi.
Có thể nói sâu hơn nếu muốn: JVM ngày xưa thì chỉ có cơ chế JIT (Just In Time – vừa đúng lúc), tức là khi nào cần mở ứng dụng thì nó mới biên dịch .class thành mã máy, khi biên dịch nó sẽ tự động tối ưu code, tối giản file, … để lần sau nhanh hơn. Sau này nó có thêm cơ chế AOT (Ahead Of Time – trước thời gian), tức là khi cài đặt ứng dụng, nó sẽ biên dịch luôn thành mã máy, để lần sau mở ứng dụng sẽ nhanh hơn. Tuy nhiên, cơ chế này sẽ làm tăng dung lượng file cài đặt, và cũng làm tăng thời gian cài đặt.
Cấu trúc chương trình Java
Trong file source code, chứa “class” (lớp)
Mỗi “class” chứa nhiều “method” (hàm) khác nhau.
Mỗi “method” chứa nhiều “statements” (dòng lệnh) khác nhau.
Ví dụ 1 file class:
```
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
```
Khi một dự án Java chạy, JVM sẽ tìm class bạn để là class đầu tiên khởi chạy, rồi sau đó tìm đến method main để chạy.
public static void main(String[] args) {
// đây là hàm đầu tiên được chạy
}
2. Bắt đầu với Java
2.0 Sắp xếp các element trong 1 chương trình Java
Element
Mô tả
Bắt buộc có
Để ở đâu
Package
package abc;
Không
Đầu file
Import
import abc;
Không
Sau package
khai báo class
public class abc {
Có
Sau import
khai báo biến
int a;
Không
Bất cứ đâu
khai báo hàm
public void abc() {
Không
Bất cứ đâu
Package là gì:
Tưởng tượng package như cây thư mục thôi, nó giúp chúng ta phân loại các file code của chúng ta, để dễ quản lý hơn. Ví dụ như chúng ta có 1 project lớn, có 1000 file code, thì chúng ta sẽ phân chúng thành các package nhỏ, ví dụ như package com.company.project1, com.company.project2, … để dễ quản lý hơn.
Thì chỉ cần nhìn vào đây, có 2 file ảnh QuocHung, nhưng người ta biết ngay là 2 file ảnh khác nhau, vì nó nằm trong 2 thư mục khác nhau. Tương tự, package cũng giúp chúng ta phân loại các file code của chúng ta, để dễ quản lý hơn.
Ví dụ package thư mục chuyên chứa các class xử lí tác vụ (như java.util), các thư mục chuyên chứa code xử lí thuật toán (như algorithm trong C++)
Với việc nhập ta sẽ cần khai báo một đối tượng Scanner thuộc package java.util. Sau đó, ta có thể sử dụng các hàm .next(), .nextInt(),… để lấy dữ liệu theo dạng token hoặc .nextLine() để lấy cả dòng.
Bài tập
Cho số bộ test, mỗi bộ test nhập vào 2 số a, b. In ra a/b làm tròn đến 2 dấu phẩy thập phân theo format như sau:
Bo test 0001: 1/2 = 0.50
Bo test 0002: 1/3 = 0.33
…
Yêu cầu sử dụng printf
Điều cần nói
Một chương trình nhập xuất cơ bản như trên cho ta mường tượng về sự hướng đối tượng chặt chẽ của Java
Ví dụ trong C++, để nhập ta có cin, để xuất có cout, trong Python để xuất có printf, trong C cũng là printf, sao Java lại phải dài tận System.out.printf như vậy ?
Đó là vì trong Java, mọi thứ đều là đối tượng, nên để xuất ra màn hình, ta phải gọi đến hàm printf của đối tượng System.out. Đây là một trong những điểm khác biệt giữa Java và các ngôn ngữ khác. Điều này thể hiện sự tường minh tuyệt đối
Ví dụ trong C++ hay Python như trên, cái gì đang in ra ? Hàm in ra được gọi từ hư không, chẳng có gì khởi nguồn cả, còn Java thì không, ta phải gọi đến hàm printf của đối tượng System.out, nó là một đối tượng thực sự, nó có một hàm printf, và ta gọi đến hàm printf của nó. Điều này thể hiện sự tường minh tuyệt đối của Java, mọi thứ đều là đối tượng, không có gì hư không cả.
Rõ ràng, chương trình bắt đầu với đối tượng là class Example, sau đó nó tự động chạy hàm main và đang giao tiếp với các đối tượng khác, ở đây là Scanner để nhập vào, và System.out để xuất ra. Điều này thể hiện sự hướng đối tượng chặt chẽ của Java, mọi thứ đều là đối tượng, và chúng đang nói chuyện, giao tiếp với nhau.
2.2. Khai báo biến, câu lệnh rẽ nhánh, vòng lặp
2.2.1. Biến
Biến trong Java chia làm 3 loại
Local Variables
Instance Variables
Static Variables
Ngoài ra biến trong Java còn dùng để chứa 2 kiểu dữ liệu primitives và references
Local Variable là biến được khai báo trong một phương thức, constructor hoặc một khối lệnh. Biến này chỉ có giá trị trong phương thức, constructor hoặc khối lệnh đó. Khi phương thức, constructor hoặc khối lệnh kết thúc, biến này sẽ bị hủy. (Ví dụ trong hàm main ở trên, biến name là một local variable, và chỉ dùng được trong hàm main đó)
Instance Variable là biến được khai báo trong một class, nhưng bên ngoài các phương thức, constructor hoặc khối lệnh. Biến này có thể được truy cập bởi bất kỳ phương thức, constructor hoặc khối lệnh nào của class đó.
Ví dụ:
publicclassExample {
public String name; // instance variablepublicvoidsayHello() {
Stringmessage="Hello " + name; // local variable
System.out.println(message);
}
}
Static Variable là biến được khai báo trong một class, nhưng bên ngoài các phương thức, constructor hoặc khối lệnh. Biến này có thể được truy cập bởi bất kỳ phương thức, constructor hoặc khối lệnh nào của class đó. Tuy nhiên, biến này chỉ có một bản thể duy nhất, không phải mỗi đối tượng sẽ có một bản thể riêng.
Như trên, nếu không có static thì mỗi class example tạo ra nhiều object con thì count là khác nhau, còn hiện tại, count là độc nhất, gắn chặt với class Example, không phải với object con của nó.
2.2.2. Câu lệnh rẽ nhánh
Nhìn chung, câu lệnh rẽ nhánh trong Java cũng giống với C và C++
Q: Có dùng được kiểu while(1) trong Java như C++ không?
A: Không, trong Java thì boolean và integer là 2 kiểu dữ liệu khác nhau, không thể dùng chung được. Trong Java, để tạo vòng lặp vô hạn, ta sẽ dùng while(true) thay cho while(1).
Lí do cho điều này là vì trong C++, 0 là false, 1 là true, 2 cũng là true, nói chung số dương là true. Điều này làm rối loạn cho người mới lập trình, không đủ tường minh, nên trong Java, boolean và integer là 2 kiểu dữ liệu khác nhau, không thể dùng chung được.
Câu hỏi
Q: Liệu viết nhiều class có phải viết hàm main hết cho chúng không
A: Không, chỉ cần viết một class có hàm main là được. Các class khác có thể không có hàm main, hoặc có hàm main nhưng không được gọi đến. Class chính có hàm main sẽ khởi động lên ,rồi gọi vào các class khác.
Một comment bắt đầu bằng hai dấu gạch //
// đây là comment
2.2.3. Vòng lặp
Nhìn chung, vòng lặp trong Java cũng giống với C và C++
Câu lệnh lặp for
for (int i = 0; i < 10; i++) {
// thực hiện câu lệnh này 10 lần
}
Câu lệnh lặp while
while (true) {
// thực hiện câu lệnh này mãi mãi
}
Câu lệnh lặp do-while
do {
// thực hiện câu lệnh này ít nhất 1 lần
} while (true);
Lệnh lặp for gồm 3 phần trong ngoặc tròn:
Phần init dùng khởi tạo biến đếm
Phần condition chỉ định điều kiện lặp tiếp
Phần increment dùng tăng, giảm biến đếm, để tới lúc nào đó điều kiện trở thành false.
Java có một vòng lặp khác, gọi là foreach nhưng vẫn dùng từ khóa for, nhưng theo cú pháp khác.
int[] a = { 1, 2, 3 };
for (int e: a)
Các kiểu dữ liệu nguyên thủy
Kiểu dữ liệu
Kích thước
Giá trị tối thiểu
Giá trị tối đa
Mô tả
byte
1 byte
-128
127
Lưu trữ các số nguyên có dấu
short
2 bytes
-32,768
32,767
Lưu trữ các số nguyên có dấu
int
4 bytes
-2,147,483,648
2,147,483,647
Lưu trữ các số nguyên có dấu
long
8 bytes
-9,223,372,036,854,775,808
9,223,372,036,854,775,807
Lưu trữ các số nguyên có dấu
float
4 bytes
1.40129846432481707e-45
3.40282346638528860e+38
Lưu trữ các số thực
double
8 bytes
4.94065645841246544e-324d
1.79769313486231570e+308d
Lưu trữ các số thực
boolean
1 bit
true
false
Lưu trữ các giá trị logic
char
2 bytes
‘\u0000’ (or 0)
‘\uffff’ (or 65,535 inclusive)
Lưu trữ các ký tự/ chữ cái Unicode
Mảng trong Java
Mảng trong Java là một tập hợp các phần tử có cùng kiểu dữ liệu. Mảng trong Java có thể chứa các kiểu dữ liệu nguyên thủy như int, float, double, char, … hoặc các đối tượng như String, … Mảng trong Java có độ dài cố định, nghĩa là khi khai báo mảng, ta phải xác định được số lượng phần tử của mảng đó. Mảng trong Java có thể là mảng một chiều, mảng hai chiều, mảng ba chiều, … Mảng trong Java có thể được khai báo như sau:
// Khai báo mảng một chiềuint[] a = newint[10];
int[] b = {1, 2, 3, 4, 5};
// Khai báo mảng hai chiềuint[][] c = newint[10][10];
int[][] d = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
Điều cần nói
Một lần nữa ta thấy được sự chặt chẽ của Java. Ở đây, như ta nhớ, để khai báo 1 biến ta phải theo syntax sau:
<kiểu dữ liệu> <tên biến> = <giá trị>;
Vậy ở đây, int[], double[] coi cả cục này là kiểu dữ liệu “mảng” (array), và a, b là tên biến, và new int[10] là giá trị của biến đó. Vậy, ta có thể hiểu là a, b là 2 biến kiểu mảng, và giá trị của nó là 2 mảng có 10 phần tử, và các phần tử đó có kiểu int.
Điều này khác với C++, vì kiểu dữ liệu là kiểu dữ liệu, chứ không có chuyện trong kiểu dữ liệu lại có cả số lượng phần tử như c++
Trong C++ trông nó như sau:
int a[10];
Các phương thức so sánh
Phương thức
Mô tả
<
Nhỏ hơn
<=
Nhỏ hơn hoặc bằng
>
Lớn hơn
>=
Lớn hơn hoặc bằng
==
Bằng
!=
Không bằng
&&
Và
||
Hoặc
!
Phủ định
? :
Toán tử 3 ngôi
Ôn lại cuối buổi
3. Tại sao Java lại ra đời, tại sao cần hướng đối tượng chứ không code hết vào 1 file
A: Trước khi Java ra đời, các ngôn ngữ lập trình khác như C/C++ đã ra đời. Tuy nhiên, các ngôn ngữ này có một số hạn chế như sau:
Các ngôn ngữ này không thể chạy trên nhiều nền tảng khác nhau, mà phải biên dịch lại cho từng nền tảng khác nhau. Ví dụ như C++ trên Windows và C++ trên Linux là 2 ngôn ngữ khác nhau, nên phải biên dịch lại.
Các ngôn ngữ này không có khái niệm về hướng đối tượng, nên không thể phát triển các ứng dụng lớn, phức tạp.
Cơ chế hướng đối tượng của Java giúp chương trình trở thành các đối tượng làm việc với nhau, giúp cho việc phát triển các ứng dụng lớn, phức tạp trở nên dễ dàng hơn. (Sẽ nói sâu hơn vào phần sau)
Ví dụ: thay vì code hết code vào 1 file HopThu, chuyên làm mọi việc xử lí email, … ta chia làm 3 class GuiThu, NhanThu, ThungRacThu, làm chỉ nhiệm vụ như tên của nó
Khi có lỗi xảy ra, ví dụ gửi thư, ta chỉ cần sửa class GuiThu, không cần sửa cả class NhanThu, ThungRacThu, … như vậy sẽ dễ dàng hơn rất nhiều. Giusp debug 1 luồng đơn giản hơn, dễ maintain, dễ scale hơn.
Nhược điểm là về lâu về lại, số lượng class sẽ tăng lên cao rất nhiều, và sẽ khó quản lý hơn.
Điều đó buộc chúng ta đặt tên file, đặt package sao cho chuẩn, cho tường minh là điều vô cùng quan trọng, để sau này dễ quản lý hơn.
4. Có thể có 2 hàm main trong 1 chương trình không
A: Có, nhưng khi chạy, JVM sẽ chỉ chạy hàm main đầu tiên, và bỏ qua các hàm main khác.
5. Tại sao cần chia JDK và JRE và JVM ra làm 3 phần
A: Để cho việc phát triển và chạy ứng dụng trở nên dễ dàng hơn. Ví dụ, khi ta phát triển ứng dụng, ta chỉ cần cài đặt JDK, và khi ta cần chạy ứng dụng, ta chỉ cần cài đặt JRE. Nếu ta cần chạy ứng dụng trên nhiều nền tảng khác nhau, ta chỉ cần cài đặt JVM cho từng nền tảng đó. Thiết bị cuối không cần cài toàn bộ thư viện để code, nó chỉ cần chạy thôi
This entry is part 1 of 2 in the series Java Memory
Mở đầu: Tại sao cần học quản lý bộ nhớ trong Java
Đã bao giờ bạn gặp phải những câu kiểu “Nháy nút refresh ở màn hình desktop máy tính cho máy chạy nhanh hơn”, hay khởi động lại điện thoại, máy tính, chạy lại ứng dụng, web để dùng mượt hơn chưa?
Nếu có, khả năng cao bạn đã gặp phải việc một ứng dụng đã không quản lý bộ nhớ tốt, điều này khiến cho bộ nhớ ngày càng sử dụng nhiều, và khi đầy thì ứng dụng bị lag. Việc ứng dụng bị lag có thể do nhiều vấn đề như, như mạng chậm, cơ sở dữ liệu, … nhưng việc bộ nhớ bị dùng full thường chiếm một phần khá lớn trong vấn đề này.
Hiểu được cách Java quản lý bộ nhớ là một điều thiết yếu để tối ưu hệ thống.
Kiểu dữ liệu nguyên thuỷ và object
Kiểu dữ liệu nguyên thuỷ trong Java có thể kể đến byte, short, int, long, char, ….
Trong Java, các đối tượng tạo từ các class như Integer, Double, Float, … Hay từ những class mà tự bạn tạo ra, thì chúng ta đang không thực sự lưu giá trị của nó, mà ta đang lưu “địa chỉ” đến giá trị thực sự của nó
Hãy thử đi từ từ lại vấn đề này, khi ta sử dụng các kiểu biến nguyên thuỷ, ví dụ khai báo như sau
byte x = 7
Thì trong Java, bản chất biến x này đang giữ giá trị thực sự, giá trị bit (00000111)
Còn một biến Object, hay biến địa chỉ thì chúng ta như đang lưu một “điều khiển”, một tham chiếu địa chỉ đến giá trị thực sự
Dog myDog = new Dog();
Okay, mong là bạn đã hơi mường tượng được điều gì đó, tiếp theo ta sẽ đến hai khái niệm là Stack và Heap
Trong bộ nhớ Java, ta thường nhắc tới 2 nơi lưu chính đó là Stack và Heap. Toàn bộ Thread trong hệ thống sẽ có các stack khác nhau, tuy nhiên share chung 1 heap
Cơ bản ở reference bên trên, hay cách lưu tham chiếu, thì giá trị thực sẽ luôn nằm ở heap
Các biến nguyên thuỷ trong Java nếu được khai báo dưới dạng local variable (biến nằm trong các hàm, …) sẽ được lưu vào stack, còn các biến lưu dạng toàn cục của 1 class (instance variable) sẽ được lưu vào heap.
Tương tự, các biến tham chiếu (reference) cũng được lưu tương tự, nếu chúng là các local variable, các biến nằm trong 1 hàm main, 1 phương thức, … chúng sẽ được lưu trong stack. (Lưu cái con trỏ trong stack), còn nếu nó là biến toàn cục class (instance variable), nó sẽ nằm trong heap (vì hiện tại, nó đang được lưu trong 1 giá trị object thực sự khác, hay là heap)
Java Pass-by-Value, Call-By-Value
Tiếp theo, ta sẽ đi đến khái niệm pass-by-value, hay call-by-value trong Java. Hiểu đơn giản, khi ta truyền các biến như 1 tham số trong 1 hàm, ta chỉ đang truyền một bản “copy” của đối tượng ban đầu đó.
Ví dụ như đoạn code sau, đoạn code sẽ vẫn in ra 5, giá trị ban đầu của a, vì khi ta truyền vào hàm kia, ta chỉ đang truyền một bản sao của biến a, có giá trị là 5, chứ không thực sự truyền a, nên a ở hàm main sẽ không bị thay đổi.
Tuy nhiên, ta lại sẽ có một vấn đề thường hay mắc lầm khác, Hãy thử xem đoạn code sau:
Đầu tiên ta có một object John, với tuổi là 20, và tên là John
Sau đó ta chạy hàm change, nơi mà ta sẽ cập nhậtbiến tuổi truyền vào thành 90, còn tên của person truyền vào thành Michael
Bạn sẽ nghĩ là, okay, giờ ta đang truyền một bản sao thôi đúng không? Vậy thì tuổi vẫn giữ là 20, còn tên vẫn giữ là John
Nhưng kết quả thật bất ngờ, tuổi thì đúng là không thay đổi 20, nhưng tên thì cập nhật thành Michael. Tại sao lại như vậy?
Ta hãy quay trở lại khái niệm vừa nhắc tới
Khi ta truyền một biến nguyên thuỷ, hay trong trường hợp trên là age, ta đang như tạo một bản sao của một tờ giấy bình thường, tờ giấy đó ghi số là 20, ta đưa cho người khác. Họ sửa chúng thành 90, 100 hay gì đi chăng nữa, thì tờ giấy ban đầu của ta vẫn là 20
Tuy nhiên, khi ta truyền một biến reference (tham chiếu) của Object person, ta đang truyền bản sao của “tham chiếu” của nó. Hay đơn giản là, ta đang tạo ra bản sao của một cái điều khiển, vậy thì cái điều khiển bản sao này khi đưa cho người khác, vẫn sẽ bật tắt được tivi ban đầu của mình.
Escaping References
Ta sẽ đi sang vấn đề. Vậy thì điều này có thể ảnh hưởng điều gì?
Một trong bốn tính chất của OOP, Encapsulation – tính đóng gói, trong đó có thể kể đến việc, ta sẽ cho toàn bộ biến trong class thành private, ẩn chúng với bên ngoài, với class khác. Để thay đổi, hay tương tác với chúng, ta chỉ tương tác qua các phương thức public mà ta cài đặt. (Ví dụ như Getter, Setter)
Tuy nhiên ta đi đến một vấn đề mới. Vì vấn đề truyền tham chiếu, truyền điều khiển thay đổi phần gốc như trên, thì với một ví dụ như trên, ta đang để private Name của Person, tuy nhiên khi gán nó cho một biến ngoài sb bằng getter, rồi thay đổi nó, thì nó cũng thay đổi giá trị được giấu bên trong class gốc.
Điều này phá huỷ việc đóng gói của chúng ta, do ta bằng 1 cách nào đó, đã thay đổi được giá trị private, mà không cần đi qua các phương thức setter hay các phương thức cho phép thay đổi của class.
Kĩ thuật defensive copying
Lúc này, có một solution được biết đến với tên defensive copying
Ta sẽ không muốn lưu một bản tham chiếu đến một cái điều khiển, nơi mà ta có thể thay đổi giá trị đang được ẩn. Mà các phương thức lấy các tham chiếu này ra, ta sẽ tạo hẳn 1 object mới, một bản sao chỉ copy giá trị của biến gốc, chứ không copy điều khiển đến biến gốc.
Như ví dụ trên, ở hàm Getter, ta chỉ trả về một biến String Builder mới, chứa dữ liệu của name, nhưng không có khả năng thay đổi name gốc
Đầu tiên, ta cần phải hiểu về Transaction trong Database. Một phiên (Transaction) trong database có thể được coi tạo lên bởi 4 tính chất ACID
Một transaction (phiên) thể hiên một nhóm các câu lệnh được chạy trong database, thông thường chứa nhiều lệnh khác nhau.
Hãy tưởng tượng bạn chuyển 1 triệu đồng từ tài khoản A sang tài khoản B. Transaction sẽ bao gồm 2 bước:
Trừ 1 triệu từ tài khoản A
Cộng 1 triệu vào tài khoản B
Nếu bước 1 thành công nhưng bước 2 thất bại (vì lý do nào đó), transaction sẽ rollback (hoàn tác) – tức là hoàn lại 1 triệu vào tài khoản A. Như vậy sẽ không có tiền nào bị “biến mất”.
Các cấp độ isolation trong Database
Read Uncommitted Isolation Level (Cấp độ “đọc chưa hoàn thành”)
Đây là cấp độ khá lỏng lẻo, khi mà ta sẽ đọc các dữ liệu chưa được commit của row. Tức là bất kì lệnh cập nhật ,thêm vào mà chưa thật sự commit vào database cũng sẽ có luôn trong phiên hiện tại của mình. Cấp độ này thường được dùng trong các hệ thống đặt vé, đặt lịch hẹn khi mà các transaction khác đang cố update tình trạng của 1 vé, kể cả khi phiên ngoài đó chưa thực thi xong, nhưng ta cũng sẽ coi như là nó đã được chạy.
Cấp độ này thường k đảm bảo tính toàn vẹn của dữ liệu đọc được, tuy nhiên nếu chúng có thể chấp nhận được thì chúng có thể được dùng để giảm bớt tình trạng deadlock, … (Ví dụ như đọc toàn bộ dân số việt nam, ta có thể chấp nhận sai số khoảng 100-200 người chẳng hạn)
Transaction A bắt đầu và thực hiện một phép UPDATE (cộng 100k vào một tài khoản). Tại thời điểm này, sự thay đổi chỉ tồn tại trong phiên của Transaction A và chưa được xác nhận (commit) vào cơ sở dữ liệu.
Transaction B bắt đầu và đọc dữ liệu từ chính tài khoản đó. Do mức cô lập (isolation level) của cơ sở dữ liệu thấp, Transaction B đã đọc phải dữ liệu “bẩn” (dirty data) – là dữ liệu đã được Transaction A thay đổi nhưng chưa được commit.
Transaction A gặp lỗi hoặc quyết định hủy bỏ, do đó nó thực hiện lệnh ROLLBACK. Mọi thay đổi của Transaction A sẽ bị hủy bỏ, và dữ liệu trong tài khoản quay trở lại giá trị ban đầu như trước khi A bắt đầu.
Kết quả: Transaction B bây giờ đang giữ một giá trị không còn tồn tại trong cơ sở dữ liệu, dẫn đến sự không nhất quán về dữ liệu.
Read Committed Isolation Level (cấp độ “đọc phải hoàn thành”)
Đặc điểm: Chỉ đọc được dữ liệu đã được commit. Đây là mức độ phổ biến nhất.
Transaction A đang cộng 100k vào tài khoản của bạn
Transaction B sẽ không thấy số tiền tăng cho đến khi A commit thành công
Transaction A bắt đầu và thực hiện một phép UPDATE (cộng 100k vào một tài khoản). Tại thời điểm này, sự thay đổi chỉ tồn tại trong phiên của Transaction A và chưa được xác nhận (commit) vào cơ sở dữ liệu.
Transaction B bắt đầu và đọc dữ liệu từ chính tài khoản đó. Do mức cô lập (isolation level) của cơ sở dữ liệu thấp, Transaction B đã đọc phải dữ liệu “bẩn” (dirty data) – là dữ liệu đã được Transaction A thay đổi nhưng chưa được commit.
Transaction A gặp lỗi hoặc quyết định hủy bỏ, do đó nó thực hiện lệnh ROLLBACK. Mọi thay đổi của Transaction A sẽ bị hủy bỏ, và dữ liệu trong tài khoản quay trở lại giá trị ban đầu như trước khi A bắt đầu.
Kết quả: Transaction B bây giờ đang giữ một giá trị không còn tồn tại trong cơ sở dữ liệu, dẫn đến sự không nhất quán về dữ liệu.
Trong Spring: Đây là mức độ mặc định khi bạn dùng @Transactional.
Repeatable Reads Isolation Level (Cấp độ “đọc lại”)
Giả sử, ta có một bảng chứa số tiền lương của nhân viên, và ta có một transaction chứa 2 lệnh như sau
Lệnh 1: Đếm số lượng nhân viên
Lệnh 2: Đếm tổng tiền lương của nhân viên
Lúc này, giả sử một trường hợp mà có một nhân viên mới được thêm vào ngay sau khi Lệnh 1 hoàn thành. Lúc này kết quả của lệnh 2 sẽ bị thay đổi (do có 1 nhân viên mới). Lúc này chúng ta thường sẽ chọn cấp độ Repeatable Read, đó là đảm bảo rằng số lượng row (hàng) nằm trong phiên hiện tại giữ nguyên giá trị cho đến khi hết transaction.
Điều này đảm bảo số lượng row nhân viên phải được đảm bảo từ đầu phiên, nên nhân viên mới kia (1 row mới) không được count vào, và lệnh 2 đảm bảo giá trị vẫn đúng.
Transaction 1 (T1) bắt đầu với mức cô lập Repeatable Read. Điều này có nghĩa là bất kỳ dữ liệu nào T1 đọc trong quá trình thực hiện sẽ giữ nguyên (nhất quán) trong suốt thời gian của transaction, ngay cả khi các transaction khác (như T2) thực hiện các thay đổi và commit.
Khi T1 thực hiện Lệnh 1 (SELECT COUNT(*) FROM Employees), nó đọc được số lượng nhân viên là 10. Tại thời điểm này, một “snapshot” (ảnh chụp nhanh) của dữ liệu được tạo ra cho T1.
Transaction 2 (T2) chèn thêm một nhân viên mới và commit. Lúc này, về mặt vật lý, cơ sở dữ liệu đã có 11 nhân viên.
Tuy nhiên, khi T1 thực hiện Lệnh 2 (SELECT SUM(Salary) FROM Employees), nhờ cơ chế Repeatable Read, nó vẫn đọc dữ liệu dựa trên “snapshot” ban đầu của nó. Điều này có nghĩa là T1 sẽ không thấy được nhân viên mới mà T2 đã thêm vào. Do đó, tổng lương vẫn là 100,000,000 VND (tương ứng với 10 nhân viên ban đầu), đảm bảo tính nhất quán (không bị non-repeatable read) trong cùng một transaction.
Serializable Isolation Level
Đây là cấp độ mạnh nhất trong isolation
Không một transaction nào khác được quyền đọc hoặc ghi cho đến khi transaction này hoàn thành
Cấp độ này giải quyết đa số vấn đề mà 3 cấp độ kia có, nhưng mà nó thường làm giới hạn, chỉ cho phép 1 query chạy 1 lúc trong hệ thống, tạo thành 1 điểm nghẽn và khó scale hệ thống hơn.
Nghe thì có vẻ Serializable giúp ta thoải mái và đảm bảo dữ liệu đúng, tuy nhiên bản chất, nó ngăn chặn mọi hành động đa luồng trong database vào 1 vùng dữ liệu, và ta chỉ chạy được 1 phiên trên 1 vùng vào 1 thời điểm
Transaction A bắt đầu và đọc dữ liệu. Khi Transaction A thực hiện các thao tác đọc và ghi, cơ sở dữ liệu sẽ áp dụng một khóa độc quyền (exclusive lock) lên các dữ liệu mà A đang truy cập.
Trong khi A đang giữ khóa, Transaction B cố gắng đọc cùng một dữ liệu. Tuy nhiên, do dữ liệu đang bị khóa bởi A ở mức độ Serializable, Transaction B sẽ bị chặn (blocked) và phải đợi cho đến khi A hoàn thành.
Khi Transaction A thực hiện COMMIT, khóa độc quyền được giải phóng.
Lúc này, Transaction B mới có thể tiếp tục và đọc được dữ liệu, đảm bảo rằng nó luôn đọc được dữ liệu ở trạng thái nhất quán và đã được commit cuối cùng.
Đây là một plugin hỗ trợ tạo 1 class Java đơn giản (POJO – Plain Old Java Object) từ JSON. Khi cài đặt, chỉ cần chuột phải vào 1 package, New -> Generate POJOS from JSON, và chọn tên class
Jump to Line
Cho phép bạn nhảy đến bất kì dòng code nào khi đang ở chế độ debug. Hiểu đơn giản, bạn có thể nhảy để chạy luôn một đoạn code mà không cần chạy các code trước đó
SonarLint
Một plugin cực kì mạnh giúp bạn tìm và sửa bug, ngoài ra nó cũng có thể detect các vấn đề bảo mật, các đoạn code không clean, … khi bạn đang viết
SonarLint hỗ trợ nhiều ngôn ngữ giúp lập trình viên viết code clean hơn và nhanh hơn
JPA Buddy
Một bộ công cụ giúp bạn làm việc với JPA dễ dàng hơn, có giao diện và khả năng sinh code để làm việc với các thư viện nổi tiếng như HIbernate, Mapstruct, Lombok, …
Maven Helper
Giúp bạn kiểm tra và tìm các dependency có vấn đề với nhau trong maven.
GenerateAllSetter
Giúp bạn nhanh chóng sinh hàm setter nhanh với giá mặc định
CodeGlance
Giúp bạn có một cái nhìn tổng quan về file code hiện tại, dễ dàng di chuyển đến các mục code bạn đang cần tìm
Ngôn ngữ lập trình Java được thiết kế để trở thành một ngôn ngữ không phụ thuộc vào nền tảng (machine-independent). Java có thể chạy trên bất kỳ nền tảng nào miễn là có máy ảo Java (Java Virtual Machine – JVM). Máy ảo Java là một chương trình có thể chạy trên nhiều nền tảng khác nhau mà không cần phải biên dịch lại. Máy ảo Java có thể chạy trên các máy tính, điện thoại, máy tính bảng, máy chủ, … Máy ảo Java có thể được cài đặt trên các hệ điều hành khác nhau như Windows, Linux, Mac OS, …
Java vừa đủ mạnh với nhiều thư viện, tính năng, bảo đảm các sự chặt chẽ, nhưng cũng đồng thời chạy rất nhanh. Java có thể được sử dụng để phát triển các ứng dụng desktop, web, mobile, game, … Java cũng là một trong những ngôn ngữ lập trình được sử dụng nhiều nhất hiện nay.
Java Virtual Machine (JVM)
Không như C/C++ khi mà code được biên dịch thì sẽ tạo thành các mã lệnh được làm cho riêng các vi xử lý khác nhau. Code java đầu tiên được biên dịch thành một dạng tổng quát – bytecode, là ngôn ngữ cho JVM chạy. Sau đó JVM mới chạy thành các ngôn ngữ máy cho nền tảng đó.
Trình tự hoạt động:
JRE – Java Runtime Environment
The Java Runtime Environment (JRE) provides the libraries, the Java Virtual Machine, and other components to run applets and applications written in the Java programming language. In addition, two key deployment technologies are part of the JRE: Java Plug-in, which enables applets to run in popular browsers; and Java Web Start, which deploys standalone applications over a network. It is also the foundation for the technologies in the Java 2 Platform, Enterprise Edition (J2EE) for enterprise software development and deployment. The JRE does not contain tools and utilities such as compilers or debuggers for developing applets and applications.
JRE – đúng như tên của nó (môi trường chạy java) chứa các thư viện, chứa cả JVM ở bên trong, và một số thành phần khác để chạy được các phần mềm Java. Hiểu đơn giản, nếu bạn có 1 file jar, 1 chương trình java, chỉ cần có JRE là bạn có thể chạy chúng.
JDK – Java Development Kit
JDK (Java Development Kit) – Bộ công cụ lập trình Java
Hiểu đơn giản thì JDK chứa JRE và thêm một số công cụ khác để hỗ trợ cho việc lập trình, compile các file code .java sang file .class (đọc thêm bên dưới)
Cấu trúc chương trình Java
Trong file source code, chứa “class” (lớp)
Mỗi “class” chứa nhiều “method” (hàm) khác nhau.
Mỗi “method” chứa nhiều “statements” (dòng lệnh) khác nhau.
Ví dụ 1 file class:
```
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
```
Khi một dự án Java chạy, JVM sẽ tìm class bạn để là class đầu tiên khởi chạy, rồi sau đó tìm đến method main để chạy.
public static void main(String[] args) {
// đây là hàm đầu tiên được chạy
}