[JAVA] Phân biệt Shallow Copy và Deep Copy

 

#1 Khái niệm Copy

Để bắt đầu, có lẽ chúng ta cần ôn lại khái niệm Copy trong Java. Cụ thể ở đây, ta cần phân biệt giữa Reference Copy (Copy tham chiếu) và Object Copy (Copy toàn bộ object).

Giả sử chúng ta có Car object và biến myCar1 tham chiếu tới object này. Nếu tạo một Reference Copy tới Car Object, ta sẽ có biến myCar2, nhưng cả myCar1 và myCar2 đều trỏ chung đến 1 object là Car object.
Nếu chúng ta copy chính Car object và khai báo một biến myCar2 tham chiếu tới object vừa được copy thì chúng ta có 2 biến tham chiếu tới 2 object riêng biệt:

Và trong bài viết này, chúng ta chỉ bàn đến Object Copy.

#2 Khái niệm object

Ở đây chúng ta có thể xem một Object như là một sản phẩm cấu thành từ nhiều Object khác. Hãy xem sơ đồ sau để có cái nhìn trực quan:

Một Object kiểu Person sẽ có 2 thuộc tính là Name và Address. Tuy nhiên 2 thuộc tính này cũng là các Object. Name và Address sẽ tiếp tục chứa các thuộc tính tương ứng (và trong số các thuộc tính này có thể là các Object…).

Tại sao chúng ta lại cần copy các Object? Nhu cầu này xuất hiện khi chúng ta muốn thay đổi hoặc di chuyển chúng mà vẫn phải đảm bảo được tính nguyên bản của các Object này. Có nhiều cách Copy object đã được nhắc đến trong bài viết này của tác giả. Tuy nhiên, hôm nay chúng ta sẽ đi sâu vào phương pháp sử dụng copy constructor.

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

#3 Shallow Copy

Shallow Copy sẽ chỉ copy Object “chính” mà không copy các Object “con”. Tức là 2 bản sao Object “chính” sẽ cùng chứa tham chiếu đến các object con. Để dễ hình dung, bạn có thể nhìn vào sơ đồ Object Person bên trên. Với Shallow Copy, sẽ có thêm một object Person mới được tạo ra, tuy nhiên 2 Object “con” là Name và Address thì vẫn thế. Khi đó, object Person mới và object Person “cũ” sẽ cùng tham chiếu tới 2 object “con” này.

public class Person {

    private Name name;

    private Address address;

    public Person(Person originalPerson) {

         this.name = originalPerson.name;

         this.address = originalPerson.address;

    }

[…]

}

Hạn chế của Shallow Copy nằm ở chỗ, 2 object “cũ” và “mới”  không hề hoạt động độc lập, ràng buộc giữa chúng chính là các Object “con” mà cả 2 cùng tham chiếu đến. Do đó, khi một object “cũ” hoặc “mới” thay đổi giá trị của các Object “con” này, kết quả sẽ ảnh hưởng tới object còn lại.

Chúng ta có thể thấy ngay được hạn chế này qua đoạn code sau:

Person mother = new Person(new Name(…), new Address(…));

[…]

Person son  = new Person(mother);

[…]

son.moveOut(new Street(…), new City(…));

Ở đây, chúng ta có 2 mẹ con – mother và son. Object son được tạo ra bằng việc shallow copy Object mother  Khi con còn bé thì 2 mẹ con sống chung với nhau, đồng nghĩa với địa chỉ nhà của mẹ và con là một. Tuy nhiên, khi con đã lớn và mua nhà riêng, địa chỉ của Object son sẽ phải thay đổi. Tuy nhiên nếu thay đổi trong trường hợp này thì địa chỉ của Object mother cũng thay đổi theo. Vì sao? Vì cả 2 object son và mother cùng tham chiếu tới một object Address.

Khi đó, sơ đồ của 2 object:

#4 Deep Copy

Khác với Shallow Copy, Deep copy sẽ copy ra một object độc lập hoàn toàn với object cũ và vẫn giữ được tính nguyên bản của nó.

Thể hiện Deep Copy trong code:

public class Person {

    private Name name;

    private Address address;

    public Person(Person otherPerson) {

         this.name    =  new Name(otherPerson.name);

         this.address =  new Address(otherPerson.address);

    }

[…]

}

Với Deep Copy, chúng ta có thể thử lại ví dụ của 2 mẹ con để thấy được kết quả.

Tuy nhiên, mọi thứ chưa dừng lại ở đây. Để thực hiện Deep Copy hoàn chỉnh, chúng ta phải tiếp tục copy tất cả các Object “con” có trong Object cần copy cho đến khi chỉ còn lại các biến Primitive Types hoặc Immutable Objects.

public class Street {

    private String name;

    private int number;

    public Street(Street otherStreet){

         this.name = otherStreet.name;

         this.number = otherStreet.number;

    }

[…]

}

Từ phần đầu bì viết, ta thấy Object Street gồm 2 instance variable là name (kiểu String) và number (kiểu int).

number thuộc primitive type, do đó không được xem là object. Khi tạo một instance variable thứ hai, chúng ta đã tự động tạo một bản copy độc lập của number. Trong khi đó, String là Immutable Object, với Immutable Object, ta không cần thực hiện Deep Copy cho chúng.

Tổng kết

Hãy lưu ý rằng Deep Copy cho phép chúng ta thay đổi các chi tiết bên trong một Object. Tuy nhiên nếu cứ làm như vậy thì chất lượng code của chúng ta sẽ ngày một đi xuống (Hãy tham khảo Open-Closed principle).

Trong lập trình hướng đối tượng, điều này đã vi phạm nguyên tắc encapsulation.

Thay vì thay đổi trực tiếp, chúng ta nên khởi tạo Object mới rồi gán giá trị thuộc tính ứng với Object con.

Person mother = new Person(new Name(…), new Address(…));

[…]

Person son  = new Person(mother);

[…]

son.moveOut(new Address(...));

Tham khảo bản gốc tại Dzone hoặc bản dịch tại blog Techmaster.

[Fun project] Xây dựng bản đồ Wifi thành phố với Raspi 3

Bài viết này sẽ đi sâu vào việc setup các thiết bị phần cứng để phục vụ cho mục đích xây dựng bản đồ Wi-fi thành phố.

Trước khi bắt tay vào làm việc, hãy check qua kết quả của project này – chính là bản đồ Wi-Fi của tác giả xây dựng.

Các Access Point hiển thị trên Google Maps… và trên Google Earth

#1 Chuẩn bị

Có 3 thiết bị chính: Raspi 3, card wifi và ăng-ten GPS

Đầu tiên, chúng ta cần Raspi 3.

raspi 3

Raspi 3 làm một máy tính Mini, thiết kế và chế tạo ở nước Anh thần thánh. Pi 3 có vi xử lý lõi tứ 1.2 Ghz, 1Gb RAM, tích hợp Wifi, bluetooth BLE, 4 cổng USB và ngõ Ethernet, kèm theo đó là 40 chân GPIO. Pi3 đủ sức chạy Linux hoặc Window IoT core hay thậm chí là Kali Linux.

Nếu bạn chưa biết Raspi 3 là gì, bạn đang lạc hậu đó! Hãy cập nhật ngay lập tức.

Tiếp theo ta cần chuẩn bị là một card wifi kết nối USB.

Ta có thể dùng card wifi tích hợp trên Pi3, tuy nhiên để thoải mái hơn trong việc sắp đặt vị trí và bắt được tín hiệu ổn định hơn, ta nên dùng một card wifi rời có ăng-ten (râu). Tác giả Scott sử dụng mẫu Alfa Network AWUS036NHA.

wifi card

Một thiết bị nữa cần quan tâm, đó là ăng-ten GPS. Pi3 không tích hợp linh kiện phục vụ cho GPS, do đó, ta cần một ăng-ten GPS kết nối USB.

Ăng-ten GPS này giúp ta xác định vị trí chính xác của mạng Wi-fi tìm được, nhờ đó, ta mới có thể xây dựng bản đồ Wi-fi có độ chính xác cao và đồng bộ danh sách các Access point chúng ta dò quét được với Google Map.

Scott sử dụng mẫu ăng-ten GlobalSat BU-353-S4

GPS

#2 Setup

Sau khi đã thu thập đủ các thiết bị phù hợp, đầu tiên hãy đảm bảo rằng Raspi 3 của bạn đã có hệ điều hành.

Cách đơn giản nhất để cài hệ điều hành cho Pi3 là làm theo hướng dẫn trên raspberrypi.org. Project lần này của chúng ta không quá phức tạp, do đó hệ điều hành Raspbian hoàn toàn có thể đáp ứng được.

Sau khi cài Raspbian cho Pi3, nhớ update nó.

sudo apt-get update
sudo apt-get upgrade -y

Sau khi update, chúng ta bắt đầu cài các package phục vụ cho dự án này, có tất cả 8 package:

sudo apt-get install -y screen gpsd libncurses5-dev libpcap-dev tcpdump libnl-dev gpsd-clients python-gps

Tiếp theo, chúng ta config gpsd. Hãy dùng một text editor để mở file gpsd theo đường dẫn /etc/default/gpsd

START_DAEMON="true"
GPSD_OPTIONS="-n"
DEVICES="/dev/ttyUSB0" 
USBAUTO="true"
GPSD_SOCKET="/var/run/gpsd.sock"

Để ý dòng DEVICES=”/dev/ttyUSB0″ nhé, chắc gì cái ăng-ten GPS ở trên kia đã cắm đúng vào cổng usb mang số hiệu USB0? Để kiểm tra xem ăng-ten GPS được cắm vào cổng USB nào, hãy tải đoạn script sau:

wget https://gist.githubusercontent.com/ScottHelme/e793516d2b322ed3a4c795a429eb5d04/raw/5829f33d29182aa92b79bcb7e76e71ab51c8fe1b/find.sh

…và chạy nó:

chmod +x find.sh


./find.sh

Sau khi chạy, đoạn script sẽ đưa ra một output có dạng:

pi@raspberrypi:~ $ ./find.sh
/dev/ttyUSB0 - Prolific_Technology_Inc._USB-Serial_Controller_D

Ở ví dụ này, ăng-ten GPS được cắm vào cổng USB số hiệu USB0. Như vậy, tôi không phải sửa file gpsd ở trên. Nếu của bạn có output khác, hãy sửa giá trị DEVICES giống như output bạn nhận được.

Mọi thứ đã sẵn sàng chưa? Chưa đâu. Còn một thứ quan trọng nhất, đó là Kismet. Hãy cài Kismet trước khi bước ra khỏi cửa:

wget http://www.kismetwireless.net/code/kismet-2016-07-R1.tar.xz
tar -xvf kismet-2016-07-R1.tar.xz
cd kismet-2016-07-R1/
./configure
make dep
make
sudo make install

Sau khi cài Kismat, chúng ta config nó thông qua file kismet.conf nằm ở đường dẫn /usr/local/etc/kismet.confNhư thường lệ, hãy mở nó bằng một text editor.

sudo gedit /usr/local/etc/kismet.conf

hoặc

sudo leafpad /usr/local/etc/kismet.conf

Bước đầu tiên khi config Kismet là lựa chọn card wifi làm việc với Kismet.

Tìm dòng # ncsource=wlan0…

Bỏ dấu hashtag – dấu #, và thay tên wlan0 bằng tên của interface đại diện cho card wifi USB ở trên. Để biết được interface nào đại diện cho card wifi USB, interface nào đại diện cho cạc wifi on-board, hãy dùng lệnh ifconfig

Sau khi config interface sẽ làm việc với Kismet, ta config tiếp phần đầu ra cho log file. Thông thường Kismet sẽ log ra khá nhiều thông tin “thừa”. Như vậy, chúng ta cần hạn chế bớt đầu ra như sau:

Ban đầu là như này:

logtypes=pcapdump,gpsxml,netxml,nettxt,alert

Chúng ta sẽ rút gọn còn như này:

logtypes=gpsxml,netxml

Như vậy, đầu ra của kismet chỉ gói gọn trong 2 định dạng là *.gspxml*.netxml

Okay, vậy là chúng ta đã hoàn thành việc setup.

#3 Kiểm tra tín hiệu GPS

Với tín hiệu GPS, hãy chắc chắn rằng ăng-ten GPS của bạn không bị che khuất bởi … trần nhà.

gps

gps 2

Tác giả ốp Pi3 với ăng-ten GPS thành 1 khối và gắn nó lên cửa sổ xe hơi, nhờ vậy, ăng-ten GPS không bị che khuất.

Bây giờ, hãy kiểm tra ăng-ten GPS này trên Terminal của Pi3 bằng một lệnh đơn giản:

cgps

Lệnh này sẽ khởi động một test client của gpsd (ta đã cài gpsd ở bước #2). Có thể bạn sẽ mất vài phút để nhận được kết quả.

Kết quả của tác giả – đã bị che mất phần Latitude và Longitude

Nếu bạn không thể nhận được kết quả test, hãy thử restart lại gpsd và đứng im trong 1 phút.

#4 Thiết lập IP tĩnh

Hiện tại thì Pi3 sẽ nhận được địa chỉ IP từ DHCP server của bất kỳ mạng nào mà nó đang kết nối tới. Tuy nhiên khi bạn cần kết nối Pi3 với Laptop để remote access thì sao?

Hãy mở dhcpd.conf và paste đoạn config sau vào cuối file.

interface eth0
static ip_address=192.168.1.5/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

Sau khi config xong, reboot lại Pi3.

#5 Chạy thử

Kismet sẽ xuất toàn bộ dữ liệu thu được ra một các files. ta có thể chọn vị trí để lưu chúng.

mkdir ~/testrun
cd ~/testrun

Thư mục testrun bây giờ sẽ lưu toàn bộ file output của Kismet, bây giờ, ta khởi chạy nó.

sudo kismet

Hãy tích vào ô “auto start Kismet Server” và bỏ tích ở ô “Show Console on the second prompt”, sau đó ấn Start.

kismet 1

kismet 2

Khi Kismet khởi động, sau một vài giây, bạn sẽ thấy giao diện sau:

kismet 3

Như vậy, kismet đang capture. Ở trung tâm màn hình, bạn có thể thấy thông tin về tọa độ GPS (tác giả đã che đi tọa độ của mình). Trong cột bên phải, bạn thấy được số lượng các mạng Wi-Fi đang bao quanh mình, hãy đợi thêm một vài phút và Kismet sẽ thu thập dữ liệu đủ nhiều để phân tích.

kismet 4

Sau vài phút, ta tắt Kismet và Kill server.

kismet 5

Một khi Kismet đã thoát, dữ liệu được lưu vào các file output nằm ngay tại folder lúc nãy ta khởi chạy Kismet. Bạn sẽ thấy có 2 file XML. Đây chính là 2 output được chúng ta giới hạn trong phần #2.

ls

Kismet-20160808-21-40-31-1.gpsxml
Kismet-20160808-21-40-31-1.netxml

Đây chỉ là dữ liệu thô, hãy xào nấu chúng thành những món ăn hoàn chỉnh.

#6 Export ra Google Maps

Để biến 2 nguyên liệu thô ở trên thành các món ăn hoàn chỉnh, chúng ta cần sự trợ giúp của một đoạn script mang tên netxml2kml.

wget https://gist.githubusercontent.com/ScottHelme/5c6869e17c3e9c8b2034dc8fc13e180b/raw/31c2d34f66748b6bd26415fd7d120c06b3d92eaf/netxml2kml.py -O netxml2kml.py

Đoạn script này được viết bằng python. Chúng ta sẽ chạy nó để convert từ xml ra kml và upload các file kml lên Google Maps.

python netxml2kml.py --kml -o output *xml

kết quả: 

Parser: Kismet-20160808-21-40-31-1.gpsxml, 0 new, 0 old
Parser: Kismet-20160808-21-40-31-1.netxml, 38 new, 0 old
Outputfile: output.*

KML export...
WPA     24
WEP     0
None    8
Other   6
Done. 38 networks

Kết quả đã được convert sang file output.kml. Ta có thể upload file này lên Google Maps.

Như vậy chúng ta đã hoàn thành việc cấu hình cũng như kiểm tra khả năng hoạt động của các thiết bị. Tuy nhiên, cuộc vui mới chỉ bắt đầu…

#7 Wardriving

Có lẽ vấn đề lớn nhất của giai đoạn này là NĂNG LƯỢNG. Chúng ta cần năng lượng để duy trì Raspi 3 đồng thời gánh thêm 2 thiết bị ngoại vi là card wifi và ăng-ten GPS. Có 2 cách để duy trì năng lượng là sử dụng pin và sử dụng đầu châm thuốc trên xe hơi.

pi 3

Tác giả gắn một cục pin 50.000mAh. Cục pin này đủ dùng cho Pi 3 trong 1 ngày đi bộ.

Khi bạn sử dụng xe hơi, hãy dùng adapter và cắm thẳng vào đầu châm thuốc trên xe.

Một vấn đề nho nhỏ, khi bạn remote access Pi 3 từ laptop thông qua SSH và chạy Kismet, nếu bạn ngắt kết nối SSH, Kismet sẽ bị đóng lại. Và chúng ta cần một công cụ hỗ trợ là Screen.

Screen là một công cụ cho phép mở nhiều shell, mỗi shell chạy một chương trình. Và quan trọng hơn hết, với Screen, khi bạn ngắt SSH, Screen session vẫn tiếp tục duy trì, nhờ đó, Kismet vẫn tiếp tục hoạt động.

Với Screen, ta khởi động một session lấy tên là kismet.

screen -S kismet

Trong session này, ta khởi chạy Kismet

sudo kismet

Khi kismet đã chạy, ta có thể nhảy ra khỏi session này bằng cách nhấn Ctrl+A, sau đó chọn D. D là viết tắc của detach.

Để kiểm tra session kismet vẫn chạy bình thường, ta gõ lệnh:

screen -ls

kết quả:
There is a screen on:
4121.kismet (Detached)
1 Socket in /var/folders/6m/xpnz0lp176n2mn/T/.screen.

Sau khi nhảy ra khỏi session kismet, ta hoàn toàn có thể quay lại nó bằng cách:

screen -r kismet

Như vậy, với screen, ta có thể chạy kismet, sau đó ngắt kết nối giữa Pi3 và Laptop rồi mang Pi 3 cùng với 2 phụ kiện còn lại đi dạo quanh thành phố….

 

Tham khảo bài viết gốc tại blog của tác giả.

Hoặc tham khảo bản dịch tại Techmaster

Java Virtual Machine và những sự thật chưa được hé lộ

Mọi lập trình viên Java đều biết rằng, bytecode sẽ dược execute trong JRE (Java Runtime Environment). Tuy nhiên rất ít người biết rằng JRE là một dạng implementation của JVM – Java Virtual Machine. JVM phân tích bytecode sau đó thông dịch và execute nó.

java-jvm-cycle

Nắm chắc kiến trúc của JVM giúp lập trình viên viết code tối ưu hơn và tự tin hơn trong các buổi phỏng vấn. Bài viết ngày hôm nay sẽ giúp bạn làm được điều đó.

#1 JVM

Virtual Machine – máy ảo, là một dạng implementation trên phần mềm của một máy tính vật lý nào đó.

Java được phát triển trên concept WORA – Write Once Run Anywhere – Viết 1 lần chạy mọi nơi. Và code Java được chạy trên máy ảo.

Bộ compiler biên dịch file *.java thành file *.class và *.class được đặt vào JVM, sau đó JVM load và execute class file đó. Biểu đồ dưới đây mô tả kiến trúc của JVM, nó sẽ theo bạn trong suốt bài viết này:

jvm-architecture

#2 Cơ chế làm việc của JVM:

Các bạn thấy đấy, JVM được chia thành 3 mô-đun chính:

  1. Class Loader
  2. Runtime Data Area
  3. Execution Engine

Và chúng ta sẽ tìm hiểu từng mô-đun một.

#2.1 Class Loader

Tính năng dynamic class loading của Java được xử lý bởi Class Loader. Bộ phận này load, link và khởi tạo (initialize) class file khi tồn tại một tham chiếu đầu tiên tới class đó trong quá trình runtime (quá trình chạy, chứ không phải quá trình biên dịch – compile time).

Tiếp tục theo dõi sơ đồ JVM, bạn sẽ thấy trong Class Loader Subsystem có 3 pha xử lý: Loading, Linking và Initialization.

Đầu tiên là pha loading, có 3 bộ loader tham gia vào việc loading: Boot Strap class Loader, Extension class Loader và Application class Loader.

  • Boot Strap Class Loader – Chịu trách nhiệm load các class từ bootstrap classpath. Bộ loader này có mức ưu tiên cao nhất.
  • Extension Class Loader – Load các class nằm trong folder jre/lib
  • Application Class Loader – load các class mức ứng dụng

Trong quá trình hoạt động, 3 bộ loader trên đều chạy dựa trên thuật toán tìm kiếm tài nguyên mang tên: Thuật toán phân cấp ủy quyền – Delegation Hierarchy Algorithm.

Thứ 2, pha linking, chia thành 3 bước như sau:

  • Verify: Bộ bytecode verifier sẽ kiểm tra đoạn bytecode được generate có hợp lệ hay không. Nếu không hợp lễ, verification error sẽ được bắn ra.
  • Prepare:  ở bước này, tất cả các biến static được cấp phát bộ nhớ và gán cho giá trị mặc định
  • Resolve: tất cả tham chiếu bộ nhớ dạng ký hiệu (symbolic memory reference) được thay thế bằng tham chiếu dạng nguyên thủy (original reference)

Thứ 3, pha initialization, là pha cuối cùng của bộ Class Loader. Tại pha này, tất cả biến static được gán giá trị (giá trị này đã được developer ghi trong file *.java) và các static block được thực thi.

970605

#2.2 Runtime Data Area

Mô-đun này được chia nhỏ thành 5 mô-đun con:

  1. Method Area – nơi lưu trữ dữ liệu mức class – tức toàn bộ các dữ liệu có trong một class sẽ nằm ở đây. Mỗi JVM chỉ có một Method Area và nó có thể được sử dụng bởi nhiều tiến trình.
  2. Heap Area – lưu trữ object và các thứ liên quan như instance variable, arrays. Giống như Method Area, mỗi JVM chỉ có một Heap Area. Vì 2 vùng này được các tiến trình chia sẻ với nhau nên dữ liệu lưu ở đây không đảm bảo thread-safe.
  3. Stack Area – Stack Area đảm bảo thread-safe bởi mỗi tiến trình sẽ được cấp phát một runtime stack. Tất cả biến cục bộ được tạo trong bộ nhớ stack. Mỗi khi có method call – lệnh gọi hàm, một “lối vào” stack sẽ được “mở”, lối vào này mang tên Stack Frame. Mỗi Stack Frame chứa 3 thực thể con:
    1. Local Variable Array – Mảng các biến cục bộ
    2. Operand Stack – ngăn xếp chứa các toán hạng
    3. Frame Data – các ký hiệu liên quan tới method được chứa ở đây, Trong trường hợp xảy ra exception, thông tin trong khối catch cũng nằm tại đây luôn.
  4. PC Registers – PC là viết tắt của Program Counter – một thanh ghi lưu địa chỉ của lệnh đang thực thi. Mỗi thread sẽ sở hữu riêng một PC.
  5. Native Method stacks – giữ các thông tin tự nhiên của method. Mỗi thread đều sở hữu một Native method stack.

#2.3 Bộ thực thi – Execution Engine

Phần bytecode được gán qua Runtime Data Area sẽ được thực thi bởi Execution Engine. Tiếp đó, mô-đun này đọc và thực thi từng đoạn byte code.

3 mô-đun con của Execution Engine là:

  1. Interpreter – Trình thông dịch. Trình thông dịch dẽ thông dịch bytecode nhanh nhưng nhược điểm là thực thi chậm. Bên cạnh đó, một nhược điểm nữa của trình thông dịch là method được gọi bao nhiêu lần thì cần bấy nhiêu lần thông dịch.
  2. JIT CompilerJust In Time Compiler. JIT Compiler sẽ trung hòa các nhược điểm của interpreter. Execution Engine dùng Interpreter để thông dịch code, và khi nó phát hiện ra code bị lặp lại, nó sẽ dùng JIT Compiler. JIT Compiler biên dịch toàn bộ bytecode (thay vì thông dịch từng dòng như interpreter) sau đó chuyển đổi thành native code. Chỗ native code này sẽ được sử dụng trực tiếp cho các lời gọi hàm lặp đi lặp lại, nhờ đó, hiệu năng được cải thiện đáng kể. Các bước xử lý của JIT Compiler gồm:
    1. Intermediate Code Generator – Sinh mã trung gian
    2. Code Optimizer – Tối ưu mã
    3. Target Code Generator – Tạo mã máy hoặc native code
    4. Profiler – Một mô-đun đặc biệt, chịu trách nhiệm tìm các “điểm nóng”, một ví dụ về “điểm nóng” là việc các lời gọi hàm bị lặp đi lặp lại.
  3. Garbage Collector – tìm kiếm và thu dọn các object đã tạo ra nhưng không được tham chiếu đến. Ta có thể kích hoạt thủ công bộ GC thông qua lệnh “System.gc()“. Tuy nhiên GC có một vài điểm yếu có thể khiến code của bạn bị memory leaks. Hãy tham khảo bài viết này để nắm được các phương pháp phòng ngừa.

2 thành phần cuối cùng của JVM là JNI Native Method Libraries:

JNI – Java Native Interface – sẽ tương tác với Native Method Libraries và cung cấp Native Libraries cần thiết cho Execution Engine.

Kiến trúc lục giác trong xây dựng ứng dụng

Bài viết này nằm trong series về kiến trúc xây dựng ứng dụng của Grzegorz Ziemonski.

#1 Tại sao lại gọi là kiến trúc lục giác?

Kiến trúc lục giác là một cách tổ chức xây dựng ứng dụng. Khác với kiến trúc phân tầng, kiến trúc lục giác chỉ chia 1 ứng dụng thành 2 vòng: vòng trong – vòng ngoài (inside – outside).

Inside – vòng trong – chứa những thành phần tương tự như 2 tầng application và domain trong kiến trúc phân tầng

Outside – vòng ngoài – chứa tất cả những gì còn lại: UI, database, hệ thống messaging,…

Phần liên kết giữa 2 vòng này được đảm nhiệm bởi “adapter” và “port“.

12-0272-03d-feature

adapter” là một implementation của “port“, “port” lại là một dạng trừu tượng hóa (abstraction) của các kết nối giữa inside và outside. “port” sẽ gắn liền với yêu cầu của “vòng trong”, việc triển khai “adapter” sẽ phụ thuộc vào “port” tương ứng và các yêu cầu đến từ “vòng ngoài”.

Một ứng dụng có thể có nhiều port và adapter. Và hình lục giác trong trường hợp này chính là một hình ảnh ẩn dụ từ đó.

2301

Kiến trúc lục giác cũng kéo theo 3 quy tắc:

  1. Vòng trong “không biết gì” về vòng ngoài
  2. Có thể implement bất kì adapter nào cho một port được yêu cầu
  3. Vòng ngoài – outside part – không chứa use case hoặc domain logic

#2 Bản chất của kiến trúc lục giác

Theo ý kiến của tác giả, khi xem xét kiến trúc lục giác, ta cần chú ý đến 2 đặc trưng của nó:

  1. Việc chia tách phần core của ứng dụng
  2. Tư duy lập trình theo “port” và “adapter

Chia tách phần core của ứng dụng đồng nghĩa với việc các use case và domain logic sẽ không xuất hiện trong các dependency ở “vòng ngoài”. Chúng ta trừu tượng hóa các business rules của ứng dụng thông qua “port”. Việc này đem lại 2 thuận lợi:

  • Phần logic xử lý của ứng dụng sẽ không bị lệ thuộc vào mô hình triển khai.
  • Dễ dàng kiểm thử phần lập trình cho logic xử lý..

Về vấn đề tư duy lập trình theo “port” và “adapter”, trong kiến trúc phân tầng, có một khác biệt lớn giữa tầng application và tầng infastructure. Tầng infrastructure luôn được đánh giá là “quan trọng hơn” bởi nó liên quan trực tiếp tới application logic và domain logic. Tuy nhiên với kiến trúc lục giác, sự khác biệt đó không tồn tại bởi các thành phần tương ứng với tầng application và infrastructure chỉ nằm ở vòng ngoài. Việc tương tác giữa 2 vòng đảm nhận bởi port và adapter. Triển khai port thường liên quan tới “vòng trong” nhiều hơn trong khi triển khai “adapter” liên quan tới “port” tương ứng và các yêu cầu xử lý của vòng ngoài.

#3 Triển khai kiến trúc lục giác qua một ví dụ đơn giản

Trong ngôn ngữ lập trình Java, có thể xem interface tương ứng với port, còn adapter thì đã được mô tả trong cuốn sách này.

Ví dụ: game Tic-tac-Toe

Trong game này, khi bạn cần ghi lại lượt đi của người chơi, bạn có thể dùng Scanner và đọc trực tiếp lượt đi đó. Tuy nhiên cách này lại khó test và nó làm phát sinh một vấn đề liên quan tới phân biệt lượt đi của máy và của người chơi.

Nếu áp dụng kiến trúc lục giác vào trường hợp này, ta sẽ tạo một “port” phụ trách liên lạc với người chơi. Khi đó, việc xử lý lượt đi mà người chơi nhập vào sẽ thuộc “vòng ngoài” và hoàn toàn độc lập với logic xử lý ở vòng trong của game.

Tạo port:

public interface PlayerPort {
    Coordinates nextMove();
}

Implement Adapter từ Port mới tạo:

public class ConsolePlayerAdapter implements PlayerPort {
 
    @Override
    public Coordinates nextMove() {
        // actual console reading stuff
    }
}

Ví dụ cũ : Pet Clinic

Tiếp theo chúng ta sẽ xét ví dụ về ứng dụng Pet Clinic đượ trình bày từ bài viết trước.

ss2017-02-12at05-21-58

Chắc chắn, controller và các config class sẽ thuộc vòng ngoài. Các domain class như Owner, Pet sẽ thuộc vòng trong. Các Repository là interface – như vậy khi chuyển sang mô hình lục giác, chúng sẽ ứng với port. Tuy nhiên ở các Repository này có thể chứa các dependency trên Spring và chứa ngôn ngữ truy vấn sử dụng @Query annotation. Tình huống này dẫn đến một câu hỏi thú vị:

Spring, JPA… có phải là một phần của “vòng ngoài”

Với việc sử dụng port và implement chúng thành các adapter vừa ý, chúng ta vừa gỡ bỏ được sự cồng kềnh trong use case và domain logic, vừa có được sự linh hoạt khi implement port ra các adapter. Tuy nhiên có người sẽ thắc mắc rằng ap dụng port và adapter mang lại sự linh hoạt nhưng lại phải thêm một s…

Theo ý kiến của tác giả,

Hãy tạm gác các vấn đề liên quan tới ràng buộc của framework và quay trở lại với ví dụ Pet Clinic.

Bước đầu tiên, chúng ta cần phân tách các class thuộc vòng trong và vòng ngoài. Với ví dụ này, ta cần tạo tầng Presentation…

ss2017-02-12at06-15-08

Tiếp theo, ta tách use case logic ra khỏi controlller. Trong ví dụ này, thử áp dụng với tính năng sắp xếp một buổi khám:

@RequestMapping(value = "/owners/{ownerId}/pets/{petId}/visits/new", method = RequestMethod.POST)
public String processNewVisitForm(@Valid Visit visit, BindingResult result) {
    if (result.hasErrors()) {
        return "pets/createOrUpdateVisitForm";
    } else {
        this.visits.save(visit);
        return "redirect:/owners/{ownerId}";
    }
}

Method này kiểm tra tính hợp lệ của request visit và trả về 1 trong 2 trang kết quả tương ứng. Như vậy, khi chuyển sang kiến trúc lục giác, ta sẽ tạo một port xử lý phần UI trong trường hợp này.

public interface UserInterfacePort {
    void promptForCorrectVisit();
    void presentOwner();
}

Tiếp theo ta có thể trừu tượng hóa phần view details và tách nó ra khỏi use case logic:

public class VisitService {
    private final VisitRepository visits;
 
    public VisitService(VisitRepository visits) {
        this.visits = visits;
    }
 
    public void scheduleNewVisit(Visit visit, BindingResult result, UserInterfacePort userInterfacePort) {
        if (result.hasErrors()) {
            userInterfacePort.promptForCorrectVisit();
        } else {
            this.visits.save(visit);
            userInterfacePort.presentOwner();
        }
    }
}

Bước cuối cùng, ta implement một adapter và sử dụng nó trong controller:

ublic class ViewNameUiAdapter implements UserInterfacePort {
    private String viewName;
 
    @Override
    public void promptForCorrectVisit() {
        this.viewName = "pets/createOrUpdateVisitForm";
    }
 
    @Override
    public void presentOwner() {
        this.viewName = "redirect:/owners/{ownerId}";
    }
 
    String getViewName() {
        return viewName;
    }
}

@RequestMapping(value = "/owners/{ownerId}/pets/{petId}/visits/new", method = RequestMethod.POST)
public String processNewVisitForm(@Valid Visit visit, BindingResult result) {
    ViewNameUiAdapter viewNameUiAdapter = new ViewNameUiAdapter();
    visitService.scheduleNewVisit(visit, result, viewNameUiAdapter);
    return viewNameUiAdapter.getViewName();
}

#4 Được lợi gì?

  • Dễ học – Chỉ cần “biết” tạo port và adapter một cách hợp lý
  • Mang lại sự “trong sạch” cho application và domain logic: phần code quan trọng nhất này sẽ không bị dính các chi tiết “phụ”
  • Linh hoạt: thể hiện qua việc implement các adapter theo bất cứ yêu cầu nào đến từ vòng ngoài
  • Dễ test: ta có thể dễ dàng viết test cho các dependecy của vòng ngoài mà không cần dùng đến các công cụ mocking.

#5 Mất gì?

  • Cồng kềnh: ngay từ ví dụ trên ta đã chia tách từ 1 method trong 1 class thành 1 interface, 3 class, 6 method
  • Khó khăn trong việc theo dõi flow of control
  • Khó khăn khi áp dụng ra các framework khác nhau
  • Không có quy chuẩn tổ chức code

#6 Áp dụng khi nào?

Cần sự “trong sạch” trong domain logic : theo ý tác giả, các ứng dụng kiểu này thường không chứa UI hoặc các chi tiết lặt vặt khác, tập tung chủ yếu vào việc tương tác dưới dạng request/response. Do đó có thể tự do sử dụng các pattern Repository, Gateway,…

Khi ứng dụng thực sự cần tính linh hoạt: việc sử dụng port và adapter rất thích hợp cho trường hợp này.

Yêu cầu ứng dụng phải đảm bảo khả năng dễ kiểm thử

Kết luận

Kiến trúc lục giác tiếp cận ứng dụng của chúng ta thông qua việc tổ chức code thành 2 vòng: vòng trong và vòng ngoài. Vòng trong chứa use case và domain logic, vòng ngoài chứa tất cả những thứ còn lại: UI, database, messaging. 2 vòng này được “kết nối” thông qua “port” và “adapter”. “port” sinh ra tương ứng với yêu cầu của “vòng trong” và “adapter” được implement từ “port” bởi vòng ngoài.

Khi áp dụng kiến túc này, chúng ta tách phần use case và domain code (được xem như phần code quan trọng nhất của ứng dụng) ra khỏi các chi tiết “gây rối”. Nhờ vậy, ứng dụng trở nên linh hoạt và dễ dàng kiểm thử.

Tham khảo bản gốc của tác giả tại TidyJava

[JAVA]Vài dòng về HashMap

how-did-bill-gates-get-started

“Cơ chế làm việc của HashMap”

Dĩ nhiên là HashMap làm việc dựa trên nguyên lý Hashing – băm. Thế nhưng chẳng ai  muốn nghe những câu trả lời chung chung kiểu như thế.

Để hiểu về Hashing, chúng ta cần nắm được 3 khái niệm: Hash function, hash valuebucket.

Hash function, hay còn gọi là hàm băm, là một hàm mà khi ta lấy đầu vào là một giá trị bất kỳ thì ở đầu ra, hash fuction sẽ cho ta một dãy code – được gọi là hash value. Mỗi đầu vào chỉ có duy nhất một hash value.

Bucket là nơi mà chúng ta lưu trữ các cặp key-value. (Map vốn là một cấu trúc dữ liệu sử dụng key-value để lưu trữ và truy xuất các phần tử). Trong HashMap, bucket dùng LinkedList để lưu trữ (LinkedList ở đây được implement riêng trong HashMap, nó khác với LinkedList trong gói java.util.LinkedList).

“Cơ chế Hashing liên quan gì tới việc lưu và truy xuất phần value trong HashMap?”

Nhiều người sẽ nghĩ rằng, phần value được lưu trong bucket và chúng được truy xuất trực tiếp thông qua phần key. Nhưng sự thực không phải như vậy.

Hãy xem class HashMap chứa những gì:

/**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
     transient Entry[] table;

Theo chân lý từ thời nguyên thủy thì Map lưu trữ dữ liệu dưới dạng key-value. truy xuất value thông qua key.

Tuy nhiên HashMap thì lưu các Object dưới dạng các instance kiểu Entry, không lưu dưới dạng key-value.

Entry class?

Hãy nhìn tiếp vào class HashMap, bạn sẽ thấy một class tên là Entry:

static class Entry<K,V> implements Map.Entry<K,V> 
 {
     final K key;
     V value;
     Entry<K,V> next;
     final int hash;
     ........
 }

Bạn thấy đấy, tại Entry class, dữ liệu mới được lưu dưới dạng key và value. Còn HashMap thì lưu các instance của Entry class. Để lưu một value vào HashMap, ta phải gọi method put().

Cơ chế làm việc của method put()

Khi bạn đọc phần implement của put(), bạn sẽ thấy:

public V put(K key, V value) 
{
    if (key == null)
       return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) 
    {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
         {
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
          }
     }
     modCount++;
     addEntry(hash, key, value, i);
     return null;
 }

Đầu tiên khối code này check key được truyền vào có bị null không. Nếu null, nó được lưu ở vị trí 0, điều này đồng nghĩa với hashCode() = 0

Tiếp đó, nếu key không bị null, nó sẽ tạo hashCode() cho key.

Method indexFor() được gọi để mang đến thông tin chính xác về vị trí lưu Entry object này.

Và đây là phần quan trọng nhất: nếu 2 object có hash code giống nhau, chúng được lưu chung 1 bucket. Để xử lý phần này, hãy nghĩ về cấu trúc dữ liệu LinkedList, mỗi nút trong Linked List đều chứa con trỏ trỏ tới nút tiếp theo. Trong Entry class cũng thế, mỗi object của Entry class đều chứa con trỏ đẻ trỏ tới object tiếp theo, cứ thế tuần tự đến hết.

Nếu có xung đột xảy ra, HashMap sẽ kiểm tra giá trị của thuộc tính tiếp theo, nếu thuộc tính tiếp theo là NULL thì nó chèn Entry object vào vị trí còn trống đó. Nếu không, HashMap duy trì vòng lặp đế khi thuộc tính tiếp theo trống thì nó mới lưu Entry object vào đó.

Ngăn chặn các key trùng nhau

HashMap không cho phép các key trùng nhau dưới mọi hình thức.

import java.util.HashMap;
import java.util.Map;
public class HashMapExample
{
	public static void main(String[] args) 
		{
			Map map = new HashMap();
			map.put(1,"Topica");   
			map.put(1,"Fpt");   
			map.put(1,"Techmaster");   
			map.put(null,"deptrai");
			System.out.println(map);  
		}
}

Đoạn code trên là một ví dụ. Ở đây, bạn cố ý put() vào HashMap các cặp key-value có key giống nhau, value khác nhau. Tuy nhiên khi chạy, bạn thấy kết quả như sau:

{null=deptrai; 1=Techmaster}

Như vậy, “Techmaster” là giá trị value cuối cùng được put vào kèm với key = 1, các value khác như “Topica” và “Fpt” đã bị “ghi đè”…

Mấu chốt ở đây chính là hàm equals().

Như đã đề cập ở đầu bài viết, bucket sử dụng cấu trúc dữ liệu LinkedList và nó là nơi lưu trữ key-value. Nếu một hashValue chỉ đến một bucket có nhiều Entry, nó sẽ duyệt LinkedList và so sánh các key của từng Entry một bằng hàm equals(). Khi key.equals() trả về true, giá trị value tương ứng sẽ được put() vào vị trí đó.

Tất cả các class đều có thể là key nếu đó được @Override hàm equals() và hashCode(), việc này cũng giúp biến các class trở thành immutable class.

Method get() làm việc như thế nào?

public V get(Object key) 
{
    if (key == null)
       return getForNullKey();
     int hash = hash(key.hashCode());
     for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) 
     {
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
             return e.value;
     }
         return null;
 }

Ta thấy một logic gần tương tự với put() cũng được áp dụng ở đây.

Đầu tiên, check hashCode() từ key.

Nếu tìm được, trả về value, nếu không, trả về null.

Nói về performance

Với HashMap, có 2 yếu tố ảnh hưởng trực tiếp tới performance là:

  • Initial Capacity – kích thước khởi tạo
  • Load factor – hệ số tải

Capacity là con số nói về số lượng bucket trong HashMap.

Load factor là chỉ số để đo xem đến ngưỡng nào thì capacity của HashMap sẽ tự động tăng lên. Khi số lượng entry trong HashMap đạt đến ngưỡng vượt quá Load Factor cũng như Initial Capacity thì HashMap sẽ được rehash, sau đó tăng số lượng bucket lên gấp đôi. Giá trị mặc định của Load Factor0.75.

Cảm ơn và hẹn gặp lại.

Bài này đã được tôi dịch và đăng trên Techmaster Blog

[DIY] Hộp cảm biến đa năng

Trong phần đầu tiên của series này, chúng ta sẽ cùng nhau làm một chiếc hộp cảm ứng. Với chiếc hộp cảm ứng này. Các bạn có thể biến bất cứ chiếc đèn hoặc quạt ở trong nhà thành một thiết bị thông minh.

Vậy “thông minh” đến mức nào?

Chúng sẽ tự động bật khi thấy bạn ở gần, và khi bạn rời khỏi thiết bị đó, nó sẽ tự tắt sau 30s.

Nó có 2 ổ cắm điện. Một ổ cắm sẽ nối ra ổ điện trong gia đình. Một ổ còn lại sẽ nối ra đèn hoặc quạt.

Demo sản phẩm:

Giới thiệu đủ rồi, chúng ta cùng bắt tay vào làm thôi.

fjmtq9pirtdm6k4-medium

#1 Khởi động

Để hoàn thành được sản phẩm này, các bạn cần bỏ ra 100k và 30 phút.

100 k để mua 3 linh kiện cơ bản:

Cảm biến hồng ngoại – PIR sensor

Mô-đun rơ le đơn kênh 5V

Mạch nguồn 5v

Và một số phụ kiện kèm theo, gồm có:

2 phích điện

2m dây điện – Mình dùng dây Vinacap 2×0.75mm

Một ít Jumper

f7mwkq9irtdm5or-large

Bạn cũng cần một tấm nhựa foamex (dày 5mm, to bằng tờ giấy A4) để dựng phần khung hộp.

fvy55yrirxtyvmj-large

#2 Vượt chướng ngại vật

Đối với các bạn mới bắt đầu (như mình chẳng hạn), bước này sẽ tiêu tốn khá nhiều thời gian.

faa5bonirxtywi2-large

Để ý nhé cảm biến hồng ngoại của chúng ta có 3 chân, ký hiệu là Vcc, OUT, GND. Thứ tự như hình vẽ. Hãy dùng Jumper nối từ chân OUT ra chân EN1 của Mô-đun relay (như hình trên).

Sau đó, hãy để ý tới 2 chân OUT và GND trên cảm biến, 2 cặp chân Vcc-GND và Vss+ Vss- trên module relay.

relay

pir_sensor_behind

Và 2 dây đen-đỏ, ứng với 2 cực âm-dương trên mạch nguồn 5v:

adapter

húng ta sẽ nối dây đỏ – ứng với cực dương của mạch nguồn với chân Vcc trên cảm biến, chân Vss+ và Vcc trên module relay.

Nối dây đen – ứng với cực âm của mạch nguồn với chân GND trên cảm biến, chân Vss- và GND trên module relay.

Để mối nối chắc chắn hơn, hãy hàn chúng lại, phun 1 ít keo nến bọc ngoài rồi lấy băng dính điện quấn quanh.

wiring_sensor_1

Bây giờ quay trở lại với mạch nguồn 5v, 2 dây màu vàng sẽ được nối với dòng điện xoay chiều từ ổ điện của nhà các bạn. Do đó, ta sẽ nối nó với 1 ổ điện:

wiringadapter

Sau đó, ta lấy 1 đoạn dây điện cỡ 24cm, nối 1 đầu cặp dây với ổ điện trên. 1 cặp đầu kia thì nối với ổ điện thứ 2 và module relay, cụ thể như hình dưới đây:

wiring_relay

Như vậy về cơ bản, thiết bị của chúng ta đã có thể hoạt động, hãy kiểm tra nó bằng cách cắm nguồn điện vào ổ được nối với adapter và cắm đèn vào ổ còn lại:

test

Sau khi test, hãy cắt tầm nhựa foamex thành các tấm và dùng keo ghép chúng lại thành chiếc hộp. Khi đã có hộp, dùng keo nến để gắn các linh kiện theo một trật tự hợp lý.

portability3

Bây giờ, để sản phẩm của chúng ta vận hành hoàn hảo, cần tùy chỉnh cảm biến hồng ngoại một chút. Hãy để ý tới 2 núm xoay ở cảm biến, có 1 núm chỉnh khoảng cách và 1 núm chỉnh thời gian delay (tức là thời gian đèn tự tắt sau khi bạn rời khỏi vị trí). Mình để cả 2 núm hướng 10 giờ.

test3

Về cơ bản là hoàn thành rồi!

[Java]4 kiểu tham chiếu và ứng dụng thực tế

Các dev Java hiếm khi để ý rằng, Java có 4 kiểu tham chiếu chính:

  • Strong reference
  • Weak reference
  • Soft reference
  • Phantom reference

Và hôm nay, tôi sẽ giúp các bạn tìm hiểu cả 4 kiểu tham chiếu trên và ứng dụng của chúng thông qua một ví dụ đơn giản nhất.

Giả sử chúng ta có 1 ứng dụng, và một mô-đun trong đó sẽ cần lấy dữ liệu ở một bảng trong Database, bảng này tên là MASTER_TABLE. Chắc chắn các bạn sẽ không để ứng dụng liên tục gọi đến database vì như vậy sẽ làm giảm hiệu năng của chính ứng dụng đó.

Thông thường, mọi người sẽ dùng cache.

Cache là một class, đầu tiên ứng dụng sẽ check phần cache xem dữ liệu có nằm ở đây không, nếu không, nó sẽ check database và đặt phần entry của dữ liệu vào cache. Từ đó, ứng dụng không cần gọi trực tiếp cơ sở dữ liệu mà chỉ cần truy cập vào cache là có thể lấy được dữ liệu cần thiết.

java

Liệu việc này có thực sự cải thiện hiệu năng?

Câu trả lời là: tùy trường hợp…

Nếu MASTER_TABLE có lượng entry khiêm tốn thì phương pháp này rất phù hợp. Nếu ngược lại, khi MASTER_TABLE có kích thước quá lớn thì phần map tạo bởi Cache sẽ ngày càng lớn theo. Và thay vì cải thiện hiệu năng, phương pháp này có thể hút cạn bộ nhớ hệ thống.

Bạn hãy tưởng tượng đến cảnh tất cả bản ghi trong MASTER_TABLE được load vào cache, khi đó kích thước của cache nó kinh khủng cỡ nào…. Lúc ấy, cache sẽ tiêu hết bộ nhớ của JVM.

Vậy chúng ta cần làm gì….

Giảm số lượng các entry hiện hữu trong cache theo 2 hướng: giới hạn số lượng entry mới và xóa bớt các entry cũ. Tuy vậy, cách này chỉ giải quyết được một nửa của vấn đề, nửa còn lại chính là việc một phần bộ nhớ trong JVM có thể bị chiếm dụng để chứa các object không được sử dụng trong thời gian dài.

Liệu có tồn tại giải pháp lý tưởng?

Có.

Nếu chúng ta có thể tạo cache “động”. Tức là cache này có thể tự động thay đổi kích thước theo nhu cầu. Để làm được điều này, chúng ta cần một số kỹ thuật đặc biệt để xóa các entry nằm trong cache quá lâu nhưng không được dùng đến.

Trong Java, để hoàn thành được nhiệm vụ này, chúng ta cần đến các kiểu tham chiếu trong gói java.lang.ref

Trước khi đi vào ví dụ thực tế, hãy dành thời gian tìm hiểu sơ bộ về 4 kiểu tham chiếu này:

Strong reference – tạm dịch: tham chiếu MẠNH.

Hầu hết mọi đoạn code Java đều có sự hiện diện của loại tham chiếu này. Với Strong reference, chúng ta có thể tạo object và gán object đó cho một tham chiếu. Chừng nào một object có tham chiếu mạnh,. nó sẽ không bị thu hồi bởi bộ GC – Garbage Collector.

Một ví dụ về Strong reference:

HelloWorld hello = new HelloWorld();

Soft Reference – tạm dịch: tham chiếu MỎNG.

Nếu một Object không có Strong reference nhưng có Soft Reference, nó có thể bị thu hồi bởi GC khi cần thiết (khi thiếu bộ nhớ). Để lấy một object đang có Soft Reference, ta có thể invoke method get(). Sau khi invoke method get(), nếu object đã bị thu hồi, get() sẽ trả về null.

Weak reference – tạm dịch : tham chiếu YẾU.

Một object không có Strong reference nhưng lại có Weak reference thì trong lần chạy tiếp theo của GC, object này sẽ bị thu hồi cho dù bộ nhớ không bị thiếu.

Phantom reference – tạm dịch: tham chiếu MA.

Đây là một kiểu tham chiếu đặc biệt, nó ám chỉ rằng object này đã “hoàn thành nhiệm vụ” và có thể được GC thu hồi. Nếu một object không có bất kỳ loại tham chiếu nào trong 3 kiểu Strong – Soft – Weak thì rất có thể nó có Phantom reference.

Quay trở lại với ví dụ đầu bài, bây giờ, chúng ta có thể init phần cache theo kiểu WeakHashMap, khi key của WeakHashMap không chứa strong reference, phần bộ nhớ đó sẽ bị GC thu hồi. Như vậy, cache của chúng ta là cache “động”, có khả năng tự tăng – giảm kích thước phù hợp với nhu cầu thực tế.

Sau đây là một đoạn code ví dụ cho 4 kiểu tham chiếu trên:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class ReferenceExample {
       private String status ="Hi I am active";
       public String getStatus() {
              return status;
       }
       public void setStatus(String status) {
              this.status = status;
       }
       @Override
       public String toString() {
              return "ReferenceExample [status=" + status + "]";
       }
       public void strongReference()
       {
              ReferenceExample ex = new ReferenceExample();
              System.out.println(ex);
       }
       public void softReference()
       {
              SoftReference ex = new SoftReference(getRefrence());
              System.out.println("Soft refrence :: " + ex.get());
       }
       public void weakReference()
       {
              int counter=0;
              WeakReference ex = new WeakReference(getRefrence());
              while(ex.get()!=null)
              {
                     counter++;
                     System.gc();
                     System.out.println("Weak reference deleted  after:: " + counter + ex.get());
              }
       }
       public void phantomReference() throws InterruptedException
       {
              final ReferenceQueue queue = new ReferenceQueue();
              PhantomReference ex = new PhantomReference(getRefrence(),queue);
              System.gc();
              queue.remove();
              System.out.println("Phantom reference deleted  after");
       }
       private ReferenceExample getRefrence()
       {
              return new ReferenceExample();
       }
       public static void main(String[] args) {
              ReferenceExample ex = new ReferenceExample();
              ex.strongReference();
              ex.softReference();
              ex.weakReference();
              try {
                     ex.phantomReference();
              } catch (InterruptedException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
              }
       }
}

Chúng ta thu được đầu ra như sau:

Output :
ReferenceExample [status=Hi I am active]
Soft refrence :: ReferenceExample [status=Hi I am active]
Weak reference deleted  after:: 1null
Phantom reference deleted  after

Như vậy, khi chúng ta tạo ra Soft Reference, nếu không gian bộ nhớ còn thoải mái thì object có Soft Reference chưa bị GC thu hồi.

Soft refrence :: ReferenceExample [status=Hi I am active]

Với Weak và Phantom Reference, chúng đều bị GC thu hồi.

 Weak reference deleted  after:: 1null
 Phantom reference deleted  after

Checkout my blog post on Techmaster