Giới thiệu

I. LỊCH SỬ PHÁT TRIỂN JAVA

1. Khởi điểm

Cuối năm 1990, James Gosling được công ty Sun Microsystems giao nhiệm vụ xây dựng một phần mền lập trình cho các mặt hàng điện dân dụng. Lục đầu Gosling và nhóm cộng sự định lựa chọn C++ nhưng nhanh chóng nhận thấy rằng C++ không thích hợp vì quá cồng kềnh và đòi hỏi rất nhiều tài nguyên (bộ nhớ, đĩa…). Gosling đã quyết định xây dựng hẳn một ngôn ngữ lập trình mới và đặt tên là Oak (Oak: cây sồi, vì trước cửa phòng làm việc của ông nhìn ra một cây sồi).

 Đầu tiên Oak được sử dụng trong dự án Xanh (Green Project) trong đó Java được sử dụng để kiểm soát các thiết bị điện dân dụng trong dự án. Sau đó Oak được sử dụng trong dự án Phim-theo-yêu cầu (Video – on – deman Project). Qua các dự án đó, Java được phát triển nhanh chóng, đến lúc này Oak được đổi tên vì có sự trùng lặp, và Java ra đời.

2. Sự phát triển

 Java được sử dụng chủ yếu trong bộ công cụ phát triển Java (Java Development Kit –JDK) như là một thư viện chuẩn, trong đó chứa trình biên dịch, thông dịch, trợ giúp, soạn tài liệu,…

Khởi đầu với JDK 1.0 vào năm 1996, JDK 1.1 được công bố vào năm 1997 với nhiều cải tiến như tăng thêm các hàm giao diện (AWT), xây dựng hệ thống thư viện dùng lại Javabeans, JFC,…Bộ Java 1.6 là lõi cho việc viết các IED (Intergrated Envirnment development) nổi tiếng như Jbuilder 2.0, Visual J++ 6.0,…

Các phiên bản Java đã phát hành:

+ JDK 1.0 (23 tháng 01, 1996)

+ JDK 1.1 (19 tháng 2, 1997)

- JDK 1.1.5 (Pumpkin) 03 tháng 12, 1997

- JDK 1.1.6 (Abigail) 24 tháng 4, 1998

- JDK 1.1.7 (Brutus) 28 tháng 9, 1998

- JDK 1.1.8 (Chelsea) 08 tháng 4, 1999

+ J2SE 1.2 (Playground) 08 tháng 12, 1998

- J2SE 1.2.1 (không có) 30 tháng 3, 1999

- J2SE 1.2.2 (Cricket) 08 tháng 7, 1999

+ J2SE 1.3 (Kestrel) 08 tháng 5, 2000

- J2SE 1.3.1 (Ladybird) 17 tháng 5, 2001

+ J2SE 1.4.0 (Merlin) 06 tháng 02, 2002

- J2SE 1.4.1 (Hopper) 16 tháng 9, 2002

- J2SE 1.4.2 (Mantis) 26 tháng 6, 2003

+ J2SE 5 (1.5.0) (Tiger) 30 tháng 9, 2004

+ Java SE 6 (còn gọi là Mustang), được công bố 11 tháng 12 năm 2006, thông tin chính tại http://java.sun.com/javase/6/. Các bản cập nhật 2 và 3 được đưa ra vào năm 2007, bản cập nhật 4 đưa ra tháng 1 năm 2008.

+ JDK 6.18, 2010

+ Java SE 7 (còn gọi là Dolphin), được bắt đầu từ tháng 8 năm 2006 và công bố ngày 28 tháng 7 năm 2011.

+ JDK 8, 18 tháng 3 năm 2014

+ Phiên bản dự kiến tiếp theo: Java 9 dự kiến ra đời năm 2016.

Khuyến nghị: Bạn nên cài đặt bản JDK 8 cùng với NetBeans 8 trở lên.

II. CÁC ĐẶC ĐIỂM CỦA JAVA

Có nhiều đặc điểm nổi bật của Java so với các ngôn ngữ khác. Dưới đây là một số đặc điểm chủ yếu nhất.

1. Đơn giản (Simple)

 Đặc điểm đầu tiên và cũng là mục địch của Java là tính đơn giản. Mục đích của những người sáng lập ra Java là để thay thế cho C và C++. Mặc dù công việc này chẳng hề đơn giản chút nào những ta cũng cần thấy rằng Java đã hạn chế được rất nhiều tính phức tạp ở trong C và C++. Hai ví dụ có thể chứng minh cho tính đơn giản của Java so với C và C++ đó là Java đã bỏ đi khái niệm con trỏ ở trong C và bỏ đi tính đa thừa kế ở trong C++.

 Một điều nữa có thể minh chứng cho tính đơn giản của Java là kích thước của bộ biên dịch cơ bản và lớp hộ trợ là rất nhỏ. Bản JDK 1.2 chỉ chiếm dung lượng 4 Mb so với vài trăm Mb của Visual C++ và Visual Basic.

2. Hướng đối tượng

 Java là một ngôn ngữ lập trình thuần tuý hướng đối tượng. Có nghĩa là trong Java không có bất cứ một biến hay một thủ tục nào được viết ở ngoài lớp. Mọi ứng dụng viết trên Java đều phải được xây dựng từ các đối tượng và thông qua các đối tượng. Nếu như trong C/C++ ta có thể tạo ra các hàm toàn cục (global function), thì ở Java chúng ta chỉ có thể tạo ra các hàm tương tự thông qua một đối tượng nào đó.

3. Phân tán

 Java được thiết kế để hỗ trợ các ứng dụng phân tán. Ứng dụng phân tán là những ứng dụng làm việc trên môi trường mạng. Trong java để xây dựng các ứng dụng như thế ta sử dụng lớp mạng (java.net). Ví dụ với lớp URL của java. Một ứng dụng Java có thể dễ dàng truy xuất đến một máy chủ ở xa. Nó có thể mở hoặc truy xuất đến các đối tượng ở xa cũng đơn giản như là truy xuất ngay trên máy tính của mình.

4. Ngôn ngữ lai

 Java là một ngôn ngữ lai vì Java là một ngôn ngữ vừa biên dịch và vừa thông dịch.

Các bạn chắc hẳn đã nhiều bạn quen thuộc với hai thuật ngữ đó. Tuy nhiên tôi chắc chắn rằng trong số các bạn vẫn có những bạn còn đang phân vân về nó. Vì vậy xin giành một chút thời gian của các bạn để chúng ta cùng làm rõ hai thuất ngữ trên.

 Trước hết ta bắt đầu từ chương trình máy tính. Ta biết rằng hầu hết chương trình máy tính luôn được viết dưới một ngôn ngữ lập trình nào đó. Tuy nhiên máy tính của ta lại không thể thực hiện trực tiếp các chương trình đó vì thực chất máy tính chỉ là sự kết hợp các mạch điện tử và nó chỉ hiểu được ngôn ngữ máy (machine language). Ngôn ngữ máy là ngôn ngữ là chỉ được tạo thành từ các số 0 và 1; 0 ứng với mức điện áp thấp và 1 ứng với mức điện áp cao.

 Vậy để một chương trình máy tính viết bằng một ngôn ngữ lập trình nào đó được thực hiện thì trước tiên nó phải được chuyển thành ngôn ngữ máy. Quá trình chuyển một chương trình thành ngôn ngữ máy đó người ta gọi đó là quá trình dịch chương trình.

 Trong dịch chương trình thì có hai cách dịch là biên dịch và thông dịch.

 Biên dịch tức là chuyển toàn bộ chương trình sang ngôn ngữ máy rồi sau đó mới thực hiện chương trình. Ta có thể hình dung cụ thể qua sơ đồ dưới đây:

Quy trình biên dịch Java

Thông dịch tức là thực hiện dịch từng dòng lệnh của chương trình sau đó sẽ thực hiện ngay dòng lệnh đó rồi lại quay về dịch và thực hiện dòng lệnh tiếp theo. Ta có thể hình dung cụ thể về quá trình thông dịch thông qua hình vẽ dưới đây.

Sơ đồ thông dịch Java

Vậy một chương trình Java vừa được biên dịch vừa được thông dịch như thế nào? Ta hình dung qua sơ đồ dưới đây:

Biên dịch và thông dịch trong Java

Như vậy, để thực hiện một chương trình Java ta cần trải qua hai bước: Bước 1: Biên dịch chương trình Java thành một dạng mã Trung gian, được gọi là mã Bytecode.

Bước 2: Thông dịch và thực hiện mã trung gian đó.

5. Kiến trúc trung tính

 Kiến trúc trung tính của Java nói đến khả năng chạy chương trình Java trên nhiều hệ điều hành và nhiều bộ vi xử lý khác nhau.

Java với khả năng thông dịch mã bytecode (được biên dịch từ mã nguôn Java) cho phép tạo ra các máy Java ảo (JVM – Java Virtual Machine) trên mỗi hệ thống. Các chương trình Java sẽ chạy trên nền trung tính của các máy ảo Java đó dễ dàng mà không phụ thuộc vào hệ điều hành và bộ vi xử lý.

Cài đặt, Dịch và Chạy

1. Cài đặt Java

Để có thể soạn thảo các chương trình Java và tiện hành thực hiện chúng ta phải cải đặt hai công cụ là JDK và trình soạn thảo code Java. Phần hướng dẫn này sẽ hướng dẫn cách download và cài đặt JDK8 và NetBeans IDE8.

Bước 1:

Download JDK8 và NetBeans IDE 8 tại ĐÂY.

Bước 2:

Cài đặt JDK8.

Bước 3:

Cài đặt NetBeans IDE8.

2. Soạn thảo, dịch và chạy chương trình Java

2.1. Soạn thảo chương trình Java

Bước 1:

Khởi động NetBeans bằng cách vào đường dẫn Start->All Programs->NetBeans IDE 8 .

Bước 2:

Trên thanh Menu chọn File -> New Project... => sẽ hiện ra hộp thoại yêu cầu chọn loại Project.

Bước 3:

Bạn chọn Java, ở mục Projects bên phải chọn Java Application rồi nhấn Next sẽ chuyển sang mục đặt tên và vị trí lưu trữ cho Project. Giả sử bạn đặt tên là First thì IDE sẽ tạo một package có tên tương ứng là First, và lớp chứa hàm main() được tạo ra cũng sẽ có tên First. Nếu bạn không muốn tích hợp sẵn lớp chứa phương thức Main() bạn bỏ chọn ở mục Create Main Class. Sau đó bạn nhấn Finish. Chương trình sẽ nạp các lựa chọn rồi mở ra trình soạn code Java.

Bước 4:

Đoạn code trên trình soạn code Java được tạo ra có dạng như sau:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package First;

/**
 *
 * @author Tên_tác_giả
 */
public class First{

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        // TODO code application logic here        
    }
    
}

Đặt câu lệnh sau vào hàm main():

System.out.println(“First Java Program”); //In dòng văn bản

2.2. Dịch và chạy chương trình Java

Nhấn phím F6 để dịch và chạy code, kết quả sẽ được hiển thị ở phần Output phía dưới.

Lưu ý rằng nếu chương trình đúng nó sẽ được dịch thành file trung gian ở dạng mã Bytecode có đuôi .java. Nếu chương trình có lỗi thì nó sẽ hiện dòng thông báo lỗi và ta sẽ phải sửa lại chương trình.

2.3. Phân tích chương trình

Trên đây là một chương trình Java rất đơn gian.

Dòng đầu tiên: public class First để khai báo một lớp gọi là lớp chính. Đây là lớp mà có chứa hàm main của chương trình. Lớp này là bằt buộc phải có đối với mọi chương trình Java kiểu ứng dụng.

public: Phạm vi của lớp là toàn cục

class  : Từ khoá để khai báo lớp

First  : Tên lớp

Dòng thứ hai: public static void main(String[] args) dùng để khai báo hàm main của chương trình. Mọi chương trình ứng dụng Java đều phải có hàm main và nó là đầu vào của chương trình.

public: Phạm vi của hàm main là toàn cục

static: Vì hàm main là hàm duy nhất trong chương trình

void: Hàm main không có kiểu

String[] args : args dùng để lưu chữ tham số dòng lênh đầu vào cho hàm main nếu như ta chạy chương trình từ dòng lệnh.

Dòng thứ ba: System.out.println(“First Java Program”); dùng để in ra dòng thông báo First Java Program.

System: Gói System. Chúng ta sẽ tìm hiểu về gói sau.

out: Lớp để xuất dữ liệu ra

println: Hàm in dòng thông báo.

Kiểu dữ liệu

Ta biết rằng mục đích của việc viết các chương trình máy tính là để giải quyết các bài toán trong thực tế. Mà các bài toán trong thực tế thì có rất nhiều các kiểu thông tin khác nhau. Vậy làm thế nào để biểu diễn được thông tin của các bài toán trong thực tế? Rõ ràng là bất cứ một ngôn ngữ lập trình nào cũng phải xây dựng nên các kiểu dữ liệu tương ứng để biểu diễn các thông tin đó. Trong Java có các kiểu dữ liệu cơ sở sau:

Kiểu dữ liệu nguyên thủy (Primitive type)

1. Kiểu dữ liệu nguyên

Kiểu dữ liệu nguyên là kiểu dữ liệu chỉ dùng để lưa chữ các số nguyên. Ta không được phép sử dụng nó để lưa các thông tin khác. Trong Java có 4 kiễu dữ liệu nguyên:

- byte: kiểu này được cấp phát vùng nhớ 8 bit, dùng để lưu trữ các số nguyên nhỏ từ -128 đến 127. Nó hữu dụng khi làm việc với luồng dữ liệu từ một mạng hoặc một file. Kiểu dữ liệu này cũng rất hữu dụng khi làm việc với dữ liệu dạng nhị phân dạng thô mà có thể không tương thích với các kiểu dữ liệu khác của Java.

- short: Kiểu dữ liệu này ít được sử dụng nhất. Nó được cấp phát vùng nhớ 16 bit và chủ yếu áp dụng cho các máy tính 16 bit.

- int: Ngược vói kiểu short, kiểu int lại là kiểu dữ liệu được sử dụng phổ biến nhất bởi tính linh hoạt và hiệu quả của nó. Nó được cấp phát một vùng nhớ 32 bit, vì thế miền giá trị của nó là rất lớn. Kiểu int có thể được dùng để lưu trữ tổng số tiền lương được trả cho tất cả các nhân viên của một công ty.

- long: Kiểu này được sử dụng đến trong trường hợp dữ liệu là các số nguyên rất lớn mà kiểu int không thể lưu trữ được, vì nó được cấp phát một vùng nhớ tới 64 bit, ví dụ như dân số của thế giới, GDP của một quốc gia (Việt Nam chẳng hạn).

Ở đây, nếu bạn quan tâm một chút, bạn sẽ hỏi là vì sao Java tạo ra nhiều kiểu dữ liệu nguyên đến thế? Vì sao Java không sử dụng chỉ một kiểu như kiểu long chẳng hạn cho dễ học?

Câu trả lời là nằm ở chỗ thông tin về các bài toán trong thực tế rất đa dạng. Có những thông tin là những số nguyên nhỏ (điểm môn hoc, ngày tháng, ...), lại có những thông tin là những số rất lớn (tiền chẳng hạn). Vì vậy việc Java tạo ra nhiều kiểu dữ liệu như vậy là để phù hợp với sự đa dạng của các thông tin trong các bài toán thực tế.

2. Kiểu dữ liệu thực

Đây là kiểu dữ liệu được dùng để lưu trữ các số thực. Trong Java có 2 kiểu dữ liệu thực:

Kiểu

Kích thước

Giải giá trị

float

32 bits

Từ -3.40292347E+38 đến 3.40292347E+38

double

64 bits

Từ -1.79799312486231570 E+308 đến 

1.79799312486231570 E+308

3. Kiểu dữ liệu char

char là kiểu dữ liệu dùng để lưu trữ các ký tự, mỗi biến kiểu char sẽ có giá trị là một ký tự Unicode có kích thước là 16 bit từ ‘\u0000’ đến ‘\uFFFF’.

4. Kiểu dữ liệu boolean

boolean là kiểu dữ liệu chỉ lưu 2 giá trị là true false. Mỗi biến kiểu boolean có kích thức là một bit. Vì vậy ta không thể chuyển kiểu dữ liệu boolean sang kiểu int và ngược lại như ở trong C.

5. Kiểu String

Cùng với những kiểu dữ liệu nguyên thủy ở trên, Java cũng hỗ trợ dữ liệu dạng chuỗi. Một chuỗi là một dãy các ký tự. Tuy nhiên, Java không hỗ trợ kiểu dữ liệu nguyên thủy để lưu trữ các chuỗi, thay vào đó nó cung cấp một lớp có tên String để tạo biến chuỗi. Lớp String thuộc gói java.lang trong Java SE API. Câu lệnh sau đây sử dụng lớp String như một kiểu dữ liệu nguyên thủy:

String str = "Ngon ngu lap trinh Java";

Biến str ở trên không phải là biến có kiểu nguyên thủy, mà nó là một đối tượng.

Đoạn mã sau minh họa việc sử dụng các kiểu dữ liệu trên.

public class EmployeeData {
    public static void main(String[] args) {
    // khai báo biến kiểu nguyên int
    int empNumber;
    // khai báo biến kiểu thực float
    float salary; 
    // khai báo và khởi tạo giá trị cho biến kiểu double
    double shareBalance = 456790.897;
    // khai báo và khởi tạo giá trị cho biến kiểu ký tự
    char gender = ‘M’;
    // khai báo và khởi tạo giá trị cho biến kiểu boolean
    boolean ownVehicle = false;
    // khởi tạo giá trị cho các biến empNumber và salary
    empNumber = 101;
    salary = 6789.50f;
    // in ra giá trị trong các biến
    System.out.println("Employee Number: “ + empNumber);
    System.out.println("Salary: " + salary);
    System.out.println("Gender: " + gender);
    System.out.println("Share Balance: " + shareBalance);
    System.out.println("Owns vehicle: " + ownVehicle);
  }
}

Lưu ý là kiểu dữ liệu thực float cần có hậu tố f đặt sau giá trị gán cho biến. Mặc định tất cả các giá trị thực đều có kiểu double trong Java. Các giá trị được gán cho các biến và được hiển thị bằng cách sử dụng hàm System.out.println().

Output của đoạn mã trên như sau:

Java: Oupt của các kiểu dữ liệu nguyên thủy

Kiểu dữ liệu tham chiếu

Trong Java, các đối tượng và các mảng là các biến tham chiếu. Khi một đối tượng hay một mảng được tạo thì một vùng nhớ tương ứng được gán cho nó và địa chỉ của vùng nhớ này sẽ được lưu vào biến tham chiếu. Nói cách khác, kiểu dữ liệu tham chiếu là một địa chỉ của một đối tượng hay một mảng được tạo trong bộ nhớ.

Hình sau thể hiện các kiểu dữ liệu tham chiếu mà Java hỗ trợ.

Java: kiểu dữ liệu tham chiếu

Bảng sau đây liệt kê và mô tả ba kiểu dữ liệu tham chiếu trong Java.

Kiểu dữ liệu Mô tả
Array Là một tập hợp các phần tử (biến) có cùng kiểu dữ liệu. Ví dụ, tên của các sinh viên trong một lớp học có thể được lưu vào một mảng
Class Là sự đóng gói của các biến thể hiện và các phương thức thể hiện
Interface Là một kiểu lớp trong Java được dùng để thực thi thừa kế

Từ khóa, tên riêng, ghi chú

1. Từ khoá (Keyword)

Cùng như các ngôn ngữ lập trình khác, Java sử dụng một tập các từ có chức năng xác định trước. Đương nhiên ta không thể sử dụng các từ này để làm các chức năng khác. Những từ này gọi là từ khoá.

Bảng dưới đây là tập hợp các từ khoá và chức năng của nó:

Từ khoá

Ý nghĩa

abstract

Dùng để khai báo một phương thức trừu tượng. Một phương thức trừu tượng sẽ không có phần mã lệnh. Nó chỉ là khung cho các lớp thừa kế từ nó

boolean

Kiễu dữ liệu logic

break

Từ khoá dùng để thoát khỏi vòng lặp

byte

Kiểu dữ liệu nguyên có giá trị là số 8 bit

case

từ khoá trong cấu trúc rẽ nhánh switch…case

catch

Từ khoá dùng để nhận về các giá trị lỗi

char

Kiểu dữ liệu ký tự

class

Khai báo lớp

const

Khai báo hằng

continue

Dùng để nhảy về lần lặp tiếp theo trong vòng lặp

default

Giá trị mặc định trong cấu trúc lựa chọn switch

do

nằm trong câu lệnh lặp do…while

else

Nằm trong câu lệnh if…else

extends

dẫn xuất lớp

final

Phương thức cuối. Phương thức là phương thức cuối sẽ không cho các lớp thừa kế định nghĩa lại phương thức này

float

Kiễu dữ liệu thực 4 byte

for

Lệnh lặp for

if

Lệnh điều kiện if

implements

Cài đặt giao tiếp

import

Mở các thư viện (tương tự #include trong C)

int

Kiễu dữ liệu nguyên 4 byte

interface

Giao tiếp

long

Kiễu dữ liệu nguyên 8 byte

new

Dùng để tạo một đối tượng mới

package

Gói

private

Từ khoá khai báo đối tượng tiếp sau là cục bộ

protected

Từ khoá khai báo một phương thức chỉ được sử dụng trong lớp dẫn xuất trực tiếp từ nó

public

Từ khoá khai báo đối tượng tiếp sau là toán cục

return

Trở về lệnh gọi hàm

short

Kiễu dữ liệu nguyên 2 byte

static

Đối tượng khai báo tiếp theo là tĩnh, tức là nó là đối tượng duy nhất

father

Chỉ đến đối tượng cha

switch

Câu lệnh lựa chọn switch

synchronized

Đồng bộ giữa các tiến trình

this

đối tượng đang xét

throw

Trả về lỗi

throws

Cho biết phương thức hay biến sẽ trả về khi có lỗi

transient

Giá trị biến được khai báo kiểu này sẽ không được lưu trữ trước khi có một đối tượng chứa nó được tạo ra

try

Thử làm để nhận lỗi

void

Báo cho biết phương thức khai báo tiếp theo sẽ không trả về giá trị

while

Lệnh lặp While

2. Tên riêng

Ngoài các từ khoá để thực hiện các chức năng nhất định, Java còn sử dụng một tập các tên riêng. Tên riêng là những tên cho trước và chúng thường chứa những giá trị nhất định.

Ví dụ: Math.PI  là tên riêng.

3. Ghi chú (Comment)

Ghi chú hay chú thích là những dòng giải thích nhằm làm cho chương trình trở lên dễ đọc và rõ nghĩa.

Trong khi viết chương trình thì việc ghi dòng ghi chú là vô cùng quan trọng vì rất có thể sau một thời gian dài chúng ta không còn làm việc với chương trình. Khí đó nếu chúng tà phải quay lại làm việc tiếp với chương trình thì chính những dòng ghi chú đó là chìa khoá vàng giúp ta lắm bắt các ý tưởng trong chương trình.

Trong Java có 3 cách để ta ghi dòng ghi chú:

Bắt đầu

Kết thúc

Mục đích

/*

*/

Nội dung ở giữa là phần ghi chú, có thể ghi được trên nhiều dòng

//

Không có

Phần còn lại của dòng là dòng ghi chú, chỉ ghi được trên một dòng

/**

*/

Nội dung ở giữa là phần ghi chú dòng cho JavaDoc

Hàm xử lý chuỗi (String)

Lớp String cung cấp khá nhiều hàm (hay phương thức) dùng cho việc thao tác chuỗi (hay xâu ký tự). Ta cần lưu ý rằng một biến chuỗi hay một hằng chuỗi thì đều được coi là một đối tượng, và vì vậy nó có thể có quyền sử dụng tất cả các hàm được xây dựng sẵn trong lớp String. Do đó, nếu một hàm nào đó trả về một chuỗi thì ta có quyền sử dụng trực tiếp một hàm nữa ngay sau hàm đó vì chuỗi trả về là một đối tượng mới.

Giả sử ta có một chuỗi str được khai báo và gán như sau: String str="ABCDEF", thì dưới đây sẽ trình bày các hàm chủ yếu mà ta có thể áp dụng trên chuỗi này.

Danh sách các hàm xử lý chuỗi

1. charAt(int index):

Hàm này trả về một ký tự ứng với chỉ số index trong chuỗi hiện thời. Ví dụ, str.charAt(3) sẽ trả về ký tự 'D'. Lưu ý rằng ký tự đầu tiên của chuỗi có chỉ số là 0.

2. compareTo(String st):

Hàm này dùng để so sánh hai đối tượng kiểu String là chuỗi hiện thời và chuỗi st. Phép so sánh sẽ trả về một số nguyên là hiệu của cặp ký tự cuối cùng đem so sánh giữa ký tự của chuỗi hiện thời với chuỗi st. Thao tác so sánh dựa trên giá trị Unicode của từng ký tự trong hai chuỗi bắt đầu từ bên trái hai chuỗi. Cụ thể, kết quả trả về là một số âm khi chuỗi st có thứ tự trong bảng chữ cái lớn hơn chuỗi hiện thời. Nếu kết quả trả về là số 0 thì st giống hệt chuỗi hiện thời. Còn nếu kết quả trả về một số dương thì chuỗi hiện thời lớn hơn chuỗi st.

String st = "ABcd";

str.compareTo(st)); //Trả về số -32 vì ký tự 'C' của str có mã là 67, còn ký tự 'c' trong chuỗi đối số có mã là 99.

Lưu ý: Unicode là một chuẩn trong đó nó cung cấp một số duy nhất cho mỗi ký tự mà không quan tâm đến nền tảng, chương trình, hay ngôn ngữ. Chuẩn Unicode được áp dụng trong lập trình bởi các hãng công nghệ lớn như Google, IBM, Apple, Microsoft, và những hãng khác.

3. compareToIgnoreCase(String st):

Phương thức này tương tự phương thức compareTo(), chỉ khác ở một điểm là nó không phân biệt chữ hoa hay thường giữa hai chuỗi đem so sánh.

"IJK".compareToIgnoreCase("ijk"); //Trả về 0.

4. concat(String st):

Hàm này có nhiệm vụ nối chuỗi st vào sau chuỗi hiện thời và trả về chính chuỗi hiện thời sau khi nối.

str.concat("123"); //Kết quả là chuỗi str chứa "ABCDEF123".

5. copyValueOf(char[] data):

Hàm sẽ sao chép dữ liệu từ mảng data sang chuỗi hiện thời.

char[] data = {'a', 'b', 'c'};

str = String.copyValueOf(data); /* Sau câu lệnh này thì biến str sẽ chứa chuỗi "abc" */

6. copyValueOf(char[] data, int offset, int count):

Hàm này có nhiệm vụ sao chép các ký tự của mảng data bắt đầu từ vị trí offset với số lượng count ký tự.

str = String.copyValueOf(data, 1, 2); //Sao chép vào biến chuỗi str hai ký tự 'b' và 'c' của mảng data

7. endsWith(String suffix):

Kiểm tra xem chuỗi có kết thúc bằng xâu suffix hay không, nếu đúng thì trả về true, ngược lại thì trả về false.

str.endWith("HI"); //Trả về true vì chuỗi str kết thúc là "HI"

8. indexOf(int ch):

Trả về một số nguyên là vị trí (theo chỉ số) xuất hiện đầu tiên của ký tự có giá trị mã Unicode là ch trong chuỗi hiện thời. Miền giá trị của ch là từ 0 đến 0xFFFF (0 đến 65536). Ta cũng có thể thay ch bằng một ký tự cụ thể. Nếu không tìm thấy thì hàm trả về -1. Ví dụ, nếu chuỗi s có nội dung là "XYZYX" thì ta có như sau:

s.indexOf(65); //Sẽ trả về -1 vì chuỗi s không có ký tự nào có mã là 65

s.indexOf('Y'); //Sẽ trả về số 1 vì đây là chỉ số của ký tự Y đầu tiên của chuỗi s

9. indexOf(int ch, int fromIndex):

Hàm này tương tự như hàm trên, chỉ khác ở một điểm là việc tìm kiếm và lấy chỉ số bắt đầu từ vị trí có chỉ số fromIndex.

s.indexOf('Y', 2); //Sẽ trả về 3 vì vị trí bắt đầu tìm kiếm là từ chỉ số 2, tức là từ ký tự thứ 2

10. indexOf(String st):

Hàm này sẽ tìm chuỗi st trong chuỗi hiện thời, trả lại vị trí xuất hiện đầu tiên của chuỗi st trong chuỗi khi tìm thấy, trả về -1 nếu không tìm thấy.

str.indexOf("CD"); //Trả về 2 vì chuỗi "CD" có trong chuỗi "ABCDEFGHI" ở vị trí chỉ số 2.

11. indexOf(String st, int fromIndex):

Hàm này tương tự hàm trên, có điểm khác là việc tìm kiếm chuỗi st bắt đầu từ vị trí chỉ số fromIndex.

str.indexOf("CD", 3); //Trả về -1 vì từ vị trí chỉ số 3 không tìm thấy chuỗi "CD"

12. lastIndexOf(int ch):

Hàm này trả lại vị trí chỉ số của ký ch cuối cùng trong chuỗi.

System.out.print("Đào tạo Lập trình viên".lastIndexOf('t')); /* In ra giá trị là 12 vì ký tự 't' cuối cùng của chuỗi có chỉ số 12 */

13. lastIndexOf(int ch, int lastIndex):

Trả lại vị trí xuất hiện cuối cùng của ký tự có mã ch (từ 0 đến 65536) trong xâu nhưng bắt đầu từ vị trí 0 đến vị trí lastIndex. Bạn cũng có thể sử dụng ch là một ký tự cụ thể.

String s3 = "ab123ab321";

s3.lastIndexOf(97, s3.length()); //Trả về 5, 97 là mã của ký tự 'a'

s3.lastIndexOf('b', s3.length()); //Trả về 6

14. lastIndexOf(String st):

Hàm này ngược với hàm thứ 10 ở trên, có nghĩa là hàm trả về vị trí xuất hiện cuối cùng của chuỗi st trong chuỗi hiện thời.

String s1="abc123bca";

s1.lastIndexOf("bc"); //Trả về số 6

15. lastIndexOf(String st, int lastIndex):

Hàm trả lại vị trí xuất hiện cuối cùng của chuỗi st trong chuỗi hiện thời bắt đầu từ vị trí 0 đến lastIndex.

String s2="abacad";

s2.lastIndexOf("a", 3); //Trả về 2

16. length():

Hàm này dùng để lấy chiều dài của chuỗi hiện thời.

String s4 = "1a2b3c";

s4.length(); //Trả về 6

17. replace(char oldChar, char newChar):

Hàm có nhiệm vụ thay thế tất cả các ký tự oldChar của chuỗi hiện thời bằng các ký tự newChar và trả về chuỗi mới tương ứng.

String s5 = "a1a2a3";

s5.replace('a', 'A'); //Sau câu lệnh này, s5 sẽ chứa chuỗi "A1A2A3"

18. startsWith(String prefix):

Hàm này sẽ kiểm tra xem xâu có bắt đầu bằng xâu prefix không, trả lại true nếu đúng, ngược lại thì trả lại false.

String s6 = "V1Study";

s6.startsWith("V1"); //Trả về true

19. substring(int beginIndex):

Hàm có nhiệm vụ trả về một chuỗi con trong chuỗi hiện thời bắt đầu từ vị trí chỉ số beginIndex.

String s7 = "America";

s7.substring(3); //Trả vể chuỗi "rica"

20. substring(int beginIndex, int endIndex):

Hàm trả lại một xâu con trong xâu hiện tại bắt đầu từ vị trí beginIndex đến vị trí endIndex-1.

s7.substring(1, 5); //Trả về "meri"

21. toCharArray():

Hàm có tác dụng chuyển chuỗi hiện thời sang một mảng ký tự nếu bạn muốn thao tác chuỗi hiện thời theo dạng mảng ký tự.

22. toLowerCase():

Chuyển tất cả các ký tự trong chuỗi hiện thời sang chữ thường.

String s8 = "VIETNAM";

s8.toLowerCase(); //s8 sẽ chứa chuỗi "vietnam"

23. toUpperCase():

Hàm chuyển tất cả các ký tự của chuỗi sang chữ hoa.

String s9 = "programming";

s9.toUpperCase(); //s9 sẽ chứa chuỗi "PROGRAMMING"

24. trim():

Tác dụng của hàm này là xoá tất cả các ký tự trắng (Space) ở đầu và cuối chuỗi hiện thời.

String s10 = "   Thuy   ";

s10.trim(); //Sau câu lệnh này thì s10 sẽ chứa "Thuy"

25. valueOf(giá_trị):

Nếu bạn muốn chuyển một giá_trị bất kỳ nào đó sang dạng chuỗi thì bạn sử dụng đến hàm này. Ví dụ dưới đây sẽ chuyển giá trị true (có kiểu boolean) thành chuỗi.

String s11 = String.valueOf(true); //Sau câu lệnh này thì s11 sẽ chứa chuỗi "true"

26. codePointAt(int index):

Hàm này trả về một số chính là thứ tự ký tự trong bảng mã ASCII của ký tự tương ứng với chỉ số index.

String s = "abcdef";
System.out.println(s.codePointAt(2)); //In ra: 99

27. codePointBefore(int index):

Hàm này tương tự như hàm codePointAt(), nhưng trả về thứ tự ký tự trong bảng mã ASCII của ký tự trước ký tự có chỉ số index.

String s = "abcdef";
System.out.println(s.codePointBefore(2)); //In ra: 98

28. contains(CharSequence s):

Hàm này dùng để tìm kiếm chuỗi con s trong chuỗi hiện thời. Nếu tìm thấy trả về true, không tìm thấy trả về false.

String str = "V1Study.com";
System.out.println(str.contains("V1")); //In ra: true
System.out.println(str.contains("VS")); //In ra: false

29. contentEquals(CharSequence s):

Hàm có nhiệm vụ so sánh chuỗi s với chuỗi hiện thời, nếu giống nhau thì hàm trả về true, ngược lại sẽ trả về false.

String s = "V1Study";
System.out.println(s.contentEquals("V1St")); //In ra: false
System.out.println(s.contentEquals("V1Study")); //In ra: true

30. equals(Object obj):

Nhiệm vụ của hàm này là để so sánh đối tượng obj với chuỗi hiện thời, nếu giống nhau thì trả về true, ngược lại sẽ trả về false. Lưu ý là việc so sánh là có tính đến ký tự hoa thường.

String s = "Study";
int n = 10;

System.out.println(s.equals("V1Study")); //In ra: false
System.out.println(s.equals(n)); //In ra: false
System.out.println(s.equals("Study")); //In ra: true

31. equalsIgnoreCase(String str):

Hàm này dùng để so sánh chuỗi str với chuỗi hiện thời mà không quan tâm đến chữ hoa hay thường.

String s = "abc";
System.out.println(s.equals("ABC")); //In ra: true

32. getBytes():

Hàm này dùng để mã hóa chuỗi hiện thời sử dụng bảng mã mặc định của hệ thống và trả về một mảng kiểu byte.

System.out.println("V1Study".getBytes()); //In ra: [B@1db9742

33. void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):

Hàm này dùng để copy nội dung của chuỗi hiện thời vào mảng ký tự dst. Nội dung copy bắt đầu từ vị trí có chỉ số srcBegin đến vị trí có chỉ số srcEnd-1, và nội dung copy được sẽ được đặt tại vị trí bắt đầu từ chỉ số dstBegin của mảng dst.

String str = "V1Study";
char[] ch = new char[30];
str.getChars(0, 3, ch, 0);
System.out.println(ch); //Sẽ in ra: V1S
str.getChars(3, str.length(), ch, 3);

System.out.println(ch); //Sẽ in ra: V1Study

34. hasCode():

Hàm này trả về mã băm cho chuỗi hiện thời.

String str = "V1Study";
System.out.println(str.hashCode()); //In ra: 498957294

35. isEmpty():

Hàm này sẽ trả về true nếu chuỗi hiện thời có kích thước bằng 0, ngược lại thì hàm trả về false.

String str = "V1Study";
System.out.println(str.isEmpty()); //In ra: false
str = "";
System.out.println(str.isEmpty()); //In ra: true

Dưới đây chúng ta sẽ tìm hiểu một số ví dụ áp dụng các hàm xử lý chuỗi trong Java.

Ví dụ về hàm xử lý chuỗi

Ví dụ 1:

Viết chương trình nhập vào một chuỗi str và đếm xem trong chuỗi đó có bao nhiêu ký tự 'a'. Chương trình được viết như sau:

import java.util.Scanner;
public class DemKyTu {
  public static void main(String[] args) {
    Scanner boNhap = new Scanner(System.in); //tạo một bộ nhập
    int count = 0; //khai báo và khởi tạo biến đếm ký tự
    String str = new String(); //tạo một chuỗi
    System.out.print("Mời bạn nhập 1 chuỗi: ");
    str = boNhap.nextLine(); //tiến hành nhập một chuỗi
    //thuật toán đếm số ký tự 'a'
    for (int i = 0; i<str.length(); i++) { //lấy từng ký tự trong chuỗi để kiểm tra
      if (str.charAt(i) == 'a') { //nếu ký tự nào bằng ký tự 'a'
        count++; //thì tăng biến đếm lên 1
      }
    }
    System.out.printf("Chuỗi \"%s\" có %d ký tự 'a'.%n", str, count); //in ra kết quả
  }
}

Ví dụ 2:

Yêu cầu người dùng nhập vào một chuỗi rồi đếm xem chuỗi đó có bao nhiều từ (mỗi từ cách nhau bằng một hoặc nhiều dấu cách). Chương trình được viết như sau:

import java.util.Scanner;
public class DemTu {
  public static void main(String[] args) {
    Scanner input = new Scanner(System.in);
    int count = 0;
    System.out.print("Nhập một chuỗi bất kỳ: ");
    String str = input.nextLine();
    for (int i = 0; i < str.length() - 1; i++) { /* lấy từng ký tự của chuỗi từ ký tự đầu tiên đến ký tự gần cuối */
      if (str.charAt(i) == ' ' && str.charAt(i + 1) != ' ') { /* nếu ký tự đem lấy là dấu cách và đồng thời ký tự sau đó không phải dấu cách */
        count++; /* thì tăng biến đếm lên 1 (đếm được 1 từ) */
      }
    }
    if(str.charAt(0) != ' ') { /* nếu ký tự đầu tiên không phải dấu cách (không thỏa mãn thuật toán tại vòng lặp for) */
      count++; //thì đếm nốt từ đầu tiên của chuỗi
    }
    System.out.printf("Chuỗi \"%s\" có %d từ.%n", str, count);
  }
}

Ví dụ 3:

Nhập vào họ và tên của một người. Kiểm tra xem người đó có phải họ "Dang" không? Sau đây là đoạn mã thực hiện yêu cầu:

import java.util.Scanner;
public class CheckTu {
  public static void main(String[] args) {
    Scanner nhapLieu = new Scanner(System.in);
    System.out.print("Mời bạn nhập họ và tên: ");
    String hovaten = nhapLieu.nextLine();
    hovaten = hovaten.trim(); //Cắt tất cả các dấu cách ở đầu và cuối chuỗi
    // Kiểm tra xem từ ở đầu có phải là "Dang" không?
    if (hovaten.startsWith("Dang")) {
      System.out.println("Họ của bạn là \"Dang\"");
    }
    else {
      System.out.println("Bạn không mang họ \"Dang\"");
    }
  }
}

Hàm toán học (Math)

Các hàm toàn học đều nằm trong thư viện Math => Để sử dụng chúng ta phải gọi qua thư viện Math.

Ví dụ:

float a=Math.sin(2);

Các thuộc tính của lớp Math:

- E: hằng số Euler (~ 2.7)

- PI: số pi (~ 3.14).

Danh sách các hàm toán học phổ biến:

- abs(x): trị tuyệt đối của x

- acos(x): arccos của x

- addExact(x,y): trả về tổng của hai số nguyên x và y. Ví dụ, câu lệnh System.out.println(Math.addExact(3,7)); sẽ in ra 10.

- asin(x): arcsin của x

- atan(x): arctang của x

- cbrt(x): căn bậc 3 (cube root) của x

- ceil(x): số nguyên nhỏ nhất không nhỏ hơn x

- copySign(x, y): trả về một số thực là số x với dấu là dấu của số y. Ví dụ, copySign(3, 2) sẽ trả về 3.0; copySign(3, -2) sẽ trả về -3.0.

- cos(x): cos của x

- decrementExact(x): trả về x - 1, với x phải là một số nguyên. Ví dụ, decrementExact(3) sẽ trả về 2; decrementExact(-3) sẽ trả về -4.

- exp(x): tính ex

- expm1(x): tính ex - 1

- floor(x): trả về số nguyên lớn nhất nhỏ hơn hoặc bằng x

- incrementExact(x): trả về x + 1, với x phải là một số nguyên. Ví dụ, incrementExact(3) sẽ trả về 4; incrementExact(-3) sẽ trả về -2.

- log(x): loga cơ số 10 của x

- max(x,y): số lớn nhất trong 2 số x,y

- min(x,y): số nhỏ nhất trong 2 số x,y 

- multiplyExact(x, y): tính tích của hai số nguyên x và y

- pow(x,y):  x mũ y

- random(): số ngẫu nhiến trong khoảng 0.0 đến 1.0

- round(x): số nguyến gần x nhất

- sin(x): sin của x

- sqrt(x): căn bậc 2 của x

- subtractExact(x, y): tính hiệu của hai số nguyên (x-y)

- tan(x): tang của x

- toDegrees(x): đổi góc x (đơn vị là radian) ra đơn vị là độ.

- toRadians(x): đổi góc x (đơn vị là độ - degree) ra đơn vị radian.

Ví dụ:

Viết chương trình nhập vào một số nguyên n và tính: A = sin(1) + sin(2) + … + sin(n).

Chương trình được viết như sau:

import java.util.Scanner;

 public class SumSin{

  public static void main(String[] args) throws Exception {

    char ch;

    int n,i;

    float Sum=0;

    String str = new String();

    Scanner input = new Scanner(System.in);

    System.out.print(" Nhập n = ");

    n = input.nextInt();

    Sum=0;

    for(int i=0; i<=n; i++)

    Sum = Sum + Math.sin(i);

    System.out.println("Tổng là: " + Sum);

  }

}

Hàm Date & Time

Để sử dụng các hàm về thời gian ta khai báo một đối tượng Calendar. Sau đó gọi các phương thức của lớp Calendar

Phương pháp lấy ngày, giờ hiện tại như sau:

- Calendar rightNow = Calendar.getInstance();

- int h=rightNow.get( Calendar.HOUR_OF_DAY); //giờ hiện tại

- int m=rightNow.get( Calendar.MINUTE); //phút hiện tại

- int s=rightNow.get( Calendar.SECOND); //giây hiện tại

- int d=rightNow.get( Calendar.DAY_OF_MONTH); //ngày hiện tại

- int mo=rightNow.get( mCalendar.MONTH); //tháng hiện tại

- int y=rightNow.get( Calendar.YEAR); //năm hiện tại

- rightNow.getFirstDayOfWeek(): lấy ngày đầu tiên của tuần (trả về 1 nếu là ở Việt Nam (thứ 2), trả về 0 nếu là ở Mỹ và tây Âu (chủ nhật)).

Ví dụ: Viết chương trình in ra ngày giờ hiện tại trên máy tính:

import java.util.*;

public class PrintDate{

   public static void main(String[] args){

       Calendar rightNow = Calendar.getInstance();

       int h=rightNow.get( Calendar.HOUR_OF_DAY);

       int m=rightNow.get( Calendar.MINUTE);

       int s=rightNow.get( Calendar.SECOND);

       int mo=rightNow.get( Calendar.MONTH);

       int d=rightNow.get( Calendar.DAY_OF_MONTH);

       int y=rightNow.get( Calendar.YEAR);

       System.out.println("Bây giờ là: " + h + " giờ " + m + " phút " + s + " giây. Ngày " + d + " tháng " + mo  + " năm " + y);

   }

}

Biến & Hằng

1. Biến

1.1. Định nghĩa

Biến: Là một đại lượng có giá trị có thể thay đổi được trong chương trình. Mỗi biến có một tên và chiếm một số ô nhớ nhất định trong bộ nhớ của máy tính khi chương trình chạy.

1.2. Khai báo biến

Cú pháp:

DataType    Name;

Trong đó:

Datatype     : Là kiểu dữ liệu của biến

Name          : Tên của biến

Ví dụ:

int a; //Khai báo biến a kiểu số nguyên (int)

float x,y; //Khai báo hai biến x,y kiểu số thực (float)

Khao báo và khởi tạo giá trị cho biến:

Khi khai báo biến ta đồng thời cũng có thể khởi tạo giá trị cho biên đó:

Ví dụ: int Lop=1;

1.3. Quy tắc đặt tên cho biến

Khi khai báo và đặt tên cho biến ta cần tuân thủ theo các quy tắc sau đây:

- Tên của biến phải bắt đầu bằng một ký tự

- Tên của biến phải không được có dấu cách, không được có các ký tự đặc biệt

- Để cho dễ nhớ thì tên của biến phải được đặt theo ý nghĩa của nó

Ví dụ:

int a; //Dùng để lưu chữ tuổi-> Không tốt 

int age; //Dùng để lưu chữ tuổi-> Tốt hơn

1.4. Vị trí khai báo và phạm vi hoạt động của biến

Khối lệnh: Trước khi nói về vị trí khai báo và phạm vi hoạt động của biến ta nhắc lại một khái niện đã khá quen thuộc đó là khối lệnh.

Trong Java: Khối lệnh là một đoạn chương trình nằm giữa 2 dấu ngoặc

{

Khối lệnh;  

}

- Vị trí khai báo biến: Trong Java các biến có thể được khai báo ở một vị trí bất kỳ trong một khối lênh nào đó

- Phạm vị hoạt động của biến: Các biến được khai báo ở khối lệnh nào thì có phạp vi hoạt động trong khối lệnh đó.

Ví dụ: Trong đoạn chương trình sau, biến i được khai báo trong 2 khối lệnh khác nhau và chương trình sẽ in ra hai giá trị khác nhau của hai biến i đó:

public class TestVariable {

  public static void main(String[] args) {

    int i;

    i=0;

    System.out.println("Bien thu nhat = " + i);

  }

  {

    int i;

    i=1;

    System.out.println("Bien thu nhat = " + i);

  }

}

2. Hằng (literal)

2.1. Định nghĩa

Hằng là một giá trị thực được sử dụng trong chương trình, được biểu diễn như chính nó chứ không phải là một giá trị của một biến hay kết quả của một biểu thức.

2.2. Các loại hằng trong Java

Trong Java có các loại hằng sau:

- Hằng số nguyên: Hằng số nguyên có thể được biểu diễn dưới dạng số thập phân (decimal), bắt phân (octal) hoặc thập lục phân (hexadecimal)

Integer

Long

Octal

hexadecimal

1

1L

01

0x1

16

16L

020

0x10

- Hằng số thực: Hằng số thực có thể được biển diện bằng số thập phân 5.232 hoặc kiểu mũ như 232.232e5. Để chỉ rõ đó là kiểu float hay double ta thêm ký tự f vàoi cuối đối với kiểu float và d đối với kiểu double.

- Hằng ký tự: Hằng ký tự là một ký tự đơn hay một chuỗi các dấu trắng (ESCAPE), được đặt trong hai dấu đơn. Chuối ESCAPE được dùng để biểu diễn các ký tự đặc biệt như tab (‘\t’) hay một động tác đặc biệt như xuống dòng (‘\n’). Bảng dưới đây liệt kê các chuỗi ESCAPE thường dùng:         

Chuỗi

Ý nghĩa

\b

Xoá lùi

\t

Tab ngang

\n

Xuống dòng

\f

Đẩy trang

\r

Dấu Enter

\’’

Dấu nháy kép

\’

Dấu nháy đơn

\\

Dấu sổ ngược

\uxxxx

Ký tự Unicode

- Hằng chuỗi ký tự: Mặc dù Java không cung cấp kiểu dữ liệu cơ sở string, ta vẫn có thể khai báo một hằng chuỗi ký tự trong chương trình. Một hằng chuỗi ký tự là một tập các ký tự được đặt giữa hai dấu nháy kép.

2.3. Khai báo hằng

Để khai báo hằng ta dùng từ khoá final đặt trước kiểu dữ liệu và tên biến. Ví dụ: final float PI=3.14159.

2.4 Quy tắc đặt tên và vị trí  khai báo hằng

Quy tắc đặt tên và vị trí khai báo hằng hoàn toàn giống với quy tắc đặt tên và vị trí khai báo biến.

Practical 1

Viết mã lệnh và kiểm thử giải thuật cho vấn đề sau đây.

Mô tả bài toán:

Chương trình tính tổng lương cho nhân viên trong đó người dùng nhập vào ID của nhân viên (gồm 6 số nguyên) và số giờ làm việc (số nguyên). ID, số giờ làm và tổng lương được in ra cho từng nhân viên. Cần lưu lại tổng lương và tổng số nhân viên được trả lương. Việc nhập liệu sẽ chấm dứt khi người dùng nhập vào mã nhân viên là 0.

Giả mã:

Khai báo các hằng:
vượt_giờ1 = 25
vượt_giờ2 = 30
vượt_giờ = 20
Khai báo các biến:
tổng_lương = 0
tổng_số_nhân_viên = 0
Tiến hành nhập mã_nhân_viên
while mã_nhân_viên != 0
  Nhập số_giờ_làm của nhân viên
  if số_giờ_làm > 40 then
    lương_nhân_viên = (số_giờ_làm - 40) * vượt_giờ2 + 5 * vượt_giờ1 + 35 * vượt_giờ 
  else if số_giờ_làm > 35 then
    lương_nhân_viên = (số_giờ_làm - 35) * vượt_giờ1 + 35 * vượt_giờ 
  else
    lương_nhân_viên = số_giờ_làm * vượt_giờ 
  endif
  endif
  In ra mã_nhân_viên , số_giờ_làm và lương_nhân_viên
  tổng_số_nhân_viên = tổng_số_nhân_viên + 1
  tổng_lương = tổng_lương + lương_nhân_viên 
  Nhập mã_nhân_viên 
endwhile
if mã_nhân_viên != 0 then
  In ra "Tổng số nhân viên được trả lương là: ", tổng_số_nhân_viên
  In ra "Tổng số tiền lương đã trả cho các nhân viên: vnd”, totalpayroll
else
  In ra "Không có nhân viên nào được nhập"
endif

Kiểm thử chương trình của bạn với dữ liệu mẫu như sau:

Nhập liệu Hiển thị
mã_nhân_viên số_giờ_làm lương_nhân_viên tổng_số_nhân_viên tổng_lương
0     0  
111222 1 20    
123456 41 855    
112233 40 825    
0     3 1700
 

Câu hỏi thêm:

Viết lại chương trình trên, trong đó bạn tạo một lớp có tên NhanVien. Lớp NhanVien sẽ có các thành phần sau đây (lưu ý là bạn cũng phải đưa vào các hàm tạo, các phương thức setter, getter và toString()):

Các biến thành phần:

- ma_nhan_vien - 6 số nguyên

- so_gio_lam - kiểu int

Các phương thức thể hiện: public double tinhLuong() - trả về lương của từng nhân viên.

Bài tập Java Basic

Bài tập 1:

Viết chương trình in ra dòng chữ "Cộng hoà xã hội chủ nghĩa Việt Nam" ở chính giữa màn hình, sau đó nếu ấn phím bất kỳ thì in tiếp dòng chữ "Độc lập - Tự do - Hanh phúc" cũng ở chính giữa màn hình.

Bài tập 2:

Viết chương trình in ra tam giác Pascal sau:

            1

         1 2 1

      1 2 3 2 1

   1 2 3 4 3 2 1

1 2 3 4 5 4 3 2 1

Bài tập 3:

Viết chương trình in ra hình trái tim bằng sao như hình vẽ sau:

 

               *  *             *   *  

       *                 *                 *

   *                                           *

 *                                               *

*                                                *

  *                                            *

     *                                     *

          *                            *

                *                *

                         *  

 

Bài tập 4:

Viết chương trình chèn thêm dòng chữ "I love Java" vào trái tim trong bài 3.

Bài tập 5:

Trung tâm V1Study thực hiện trao học bổng cho các học viên xuất sắc và đáp ứng đủ các yêu cầu sau:

a/ Là học viên đăng ký khóa học FULL

b/ Có điểm tổng kết >= 75%

c/ Không vi phạm nội quy của trung tâm

d/ Các kỳ thi chỉ thi lần đầu tiên

Các dữ liệu a, b, c, d ở trên của mỗi học viên được nhập từ bàn phím.

Viết chương trình nhập vào thông tin của các học viên và lưu vào ArrayList. Liệt kê những học viên đạt học bổng.

Bài tập 6:

Viết một ứng dụng Java nhập vào hai số từ người dùng (sử dụng lớp Scanner), sau đó tính thương số của số thư nhất với số thứ hai, rồi in kết quả ra màn hình.

Sử dụng try-catch để bắt ngoại lệ khi người dùng nhập liệu rồi đưa ra thông báo thích hợp dạng như sau ra màn hình:

- Số thứ nhất là một chuỗi chứ không phải một số.

- Số thứ hai phải là một số.

- Số thứ hai là 0 (gây ra phép chia cho 0).

Bài tập 7:

Viết mã lệnh và kiểm thử giải thuật cho vấn đề sau đây.

Mô tả bài toán:

Chương trình tính tổng lương cho nhân viên trong đó người dùng nhập vào ID của nhân viên (gồm 6 số nguyên) và số giờ làm việc (số nguyên). ID, số giờ làm và tổng lương được in ra cho từng nhân viên. Cần lưu lại tổng lương và tổng số nhân viên được trả lương. Việc nhập liệu sẽ chấm dứt khi người dùng nhập vào mã nhân viên là 0.

Giả mã:

Khai báo các hằng:
vượt_giờ1 = 25
vượt_giờ2 = 30
vượt_giờ = 20
Khai báo các biến:
tổng_lương = 0
tổng_số_nhân_viên = 0
Tiến hành nhập mã_nhân_viên
while mã_nhân_viên != 0
  Nhập số_giờ_làm của nhân viên
  if số_giờ_làm > 40 then
    lương_nhân_viên = (số_giờ_làm - 40) * vượt_giờ2 + 5 * vượt_giờ1 + 35 * vượt_giờ 
  else if số_giờ_làm > 35 then
    lương_nhân_viên = (số_giờ_làm - 35) * vượt_giờ1 + 35 * vượt_giờ 
  else
    lương_nhân_viên = số_giờ_làm * vượt_giờ 
  endif
  endif
  In ra mã_nhân_viên , số_giờ_làm và lương_nhân_viên
  tổng_số_nhân_viên = tổng_số_nhân_viên + 1
  tổng_lương = tổng_lương + lương_nhân_viên 
  Nhập mã_nhân_viên 
endwhile
if mã_nhân_viên != 0 then
  In ra "Tổng số nhân viên được trả lương là: ", tổng_số_nhân_viên
  In ra "Tổng số tiền lương đã trả cho các nhân viên: vnd”, totalpayroll
else
  In ra "Không có nhân viên nào được nhập"
endif

Kiểm thử chương trình của bạn với dữ liệu mẫu như sau:

Nhập liệu Hiển thị
mã_nhân_viên số_giờ_làm lương_nhân_viên tổng_số_nhân_viên tổng_lương
0     0  
111222 1 20    
123456 41 855    
112233 40 825    
0     3 1700
 

Câu hỏi thêm:

Viết lại chương trình trên, trong đó bạn tạo một lớp có tên NhanVien. Lớp NhanVien sẽ có các thành phần sau đây (lưu ý là bạn cũng phải đưa vào các hàm tạo, các phương thức setter, getter và toString()):

Các biến thành phần:

- ma_nhan_vien - 6 số nguyên

- so_gio_lam - kiểu int

Các phương thức thể hiện: public double tinhLuong() - trả về lương của từng nhân viên.

Bài tập Java Basic - Vòng lặp

1. Viết chương trình in ra bảng mã ASCII

2. Viết chương trình tính tổng bậc 3 của N số nguyên đầu tiên.

3. Viết chương trình nhập vào một số nguyên rồi in ra tất cả các ước số của số đó.

4. Viết chương trình vẽ một tam giác cân bằng các dấu *

5. Viết chương trình tính tổng nghịch đảo của N số nguyên đầu tiên theo công thức

S = 1 + 1/2 + 1/3 + … + 1/N

6. Viết chương trình tính tổng bình phương các số lẻ từ 1 đến N.

7. Viết chương trình nhập vào N số nguyên, tìm số lớn nhất, số nhỏ nhất.

8. Viết chương trình nhập vào N rồi tính giai thừa của N.

9. Viết chương trình tìm USCLN, BSCNN của 2 số.

10. Viết chương trình vẽ một tam giác cân rỗng bằng các dấu *.

11. Viết chương trình vẽ hình chữ nhật rỗng bằng các dấu *.

12. Viết chương trình nhập vào một số và kiểm tra xem số đó có phải là số nguyên tố hay không?

13. Viết chương trình tính số hạng thứ n của dãy Fibonaci.

Dãy Fibonaci là dãy số gồm các số hạng p(n) với:

p(n) = p(n-1) + p(n-2) với n>2 và p(1) = p(2) = 1

Dãy Fibonaci sẽ là: 1 1 2 3 5 8 13 21 34 55 89 144…

14. Viết chương trình tính giá trị của đa thức

Pn = anxn + an-1xn-1 + … + a1x1 + a0

Hướng dẫn đa thức có thể viết lại

Pn = (…(anx + an-1)x + an-2)x + … + a0

Như vậy trước tiên tính anx + an-1, lấy kết quả nhân với x, sau đó lấy kết quả nhân với x cộng thêm an-2, lấy kết quả nhân với x … n gọi là bậc của đa thức.

15. Viết chương trình tính xn với x, n được nhập vào từ bàn phím.

16. Viết chương trình nhập vào 1 số từ 0 đến 9. In ra chữ số tương ứng. Ví dụ: nhập vào số 5, in ra "Năm".

17. Viết chương trình phân tích một số nguyên N thành tích của các thừa số nguyên tố.

18. Viết chương trình lặp lại nhiều lần công việc nhập một ký tự và in ra mã ASCII của ký tự đó, khi nào nhập số 0 thì dừng.

19. Viết chương trình tìm ước số chung lớn nhất và bội số chung nhỏ nhất của 2 số nguyên.

21. Viết chương trình tính dân số của một thành phố sau 10 năm nữa, biết rằng dân số hiện nay là 6.000.000, tỉ lệ tăng dân số hàng năm là 1.8% .

22. Viết chương trình tìm các số nguyên gồm 3 chữ số sao cho tích của 3 chữ số bằng tổng 3 chữ số. Ví dụ: 1*2*3 = 1+2+3.

23. Viết chương trình tìm các số nguyên a, b, c, d khác nhau trong khoảng từ 0 tới 10 thỏa mãn điều kiện a*d*d = b*c*c*c

24. Viết chương trình tính tổ hợp N chập K (với K <= N)

C=((N-k+1) * (N-k+2)*…N)/1*2*3*…*k

Trong đó C là một tích gồm k phần tử với phần tử thứ I là (N-k+1)/I. Để viết chương trình này, bạn dùng vòng lặp For với biến điều khiển I từ giá trị đầu là 1 tăng đến giá trị cuối là k kết hợp với việc nhân dồn vào kết quả C.

25. Viết chương trình giải bài toán cổ điển sau:

Trăm trâu, trăm cỏ

Trâu đứng ăn năm

Trâu nằm ăn ba,

Ba trâu già ăn một

Hỏi mỗi loại trâu có bao nhiêu con.

26. Viết chương trình giải bài toán cổ điển sau:

Vừa gà vừa chó

Bó lại cho tròn,

Ba mươi sáu con

100 chân chẵn

Hỏi có bao nhiêu gà, bao nhiêu chó

27. Viết chương trình in ra bảng cửu chương

28. Viết chương trình xác định xem một tờ giấy có độ dày 0.1 mm. Phải gấp đôi tờ giấy bao nhiêu lần để nó có độ dày 1m.

29. Viết chương trình tìm các số nguyên tố từ 2 đến N, với N được nhập vào.

30. Viết chương trình lặp đi lặp lại các công việc sau:

- Nhập vào một ký tự trên bàn phím.

- Nếu là chữ thường thì in ra chính nó và chữ HOA tương ứng.

- Nếu là chữ HOA thì in ra chính nó và chữ thường tường ứng.

- Nếu là ký số thì in ra chính nó.

- Nếu là một ký tự điều khiển thì kết thúc chương trình

31. Viết chương trình nhập vào x, n tính:

- sprt(x + sqrt(x + (sqrt(x))) (n dấu căn)

- 1 + x/2 + ... x^n / (x+1)

32. Viết chương trình nhập vào N số nguyên, đếm xem có bao nhiêu số âm, bao nhiêu số dương và bao nhiêu số không

33. Viết chương trình xác định tất cả các cặp số nguyên dương (A, B) sao cho A<B<1000 và (A2+B2+1)/AB là một số nguyên

34. Viết chương trình trò chơi Hi-Lo.

Máy tính nghĩ ra một số nguyên dương bất kỳ (có thể cho một giới hạn là số phải nhỏ hơn MAX_INT). Người chơi sẽ đoán một số. Máy tính sẽ trả lời: "Số lớn hơn số phải tìm", "Số nhỏ hơn số phải tìm" hoặc "Chính xác, bạn đã thắng". Sau khi đã đoán đúng số cần tìm, máy tính sẽ tính và xuất ra số lần đoán và dừng chương trình.

35. Nhập một số nguyên dương vào từ bàn phím, sau đó in ra màn hình tất cả các số nguyên tố nằm trong khoảng từ 0 cho đến số nguyên dương đó.

36. Tìm tất cả các ước số của một số nguyên dương n

37. Tìm tất cả các phương án kết hợp 3 loại giấy bạc 1000đ, 2000đ, 5000đ với nhau để cho ra số tiền n đ. Với n nhập từ bàn phím. N>=1000đ. Đưa ra phương án tối ưu ứng với số tờ tiền là ít nhất.

38. Kiểm tra số nguyên n có phải số chính phương hay ko

39. Tính tổng của các chữ số của một số tự nhiên n

40. Hiển thị các chữ số của một số tự nhiên n theo thứ tự từ phải sang trái

41. Tìm chữ số lớn nhất của một số tự nhiên n.

Phép toán (Operator)

Phép toán số học

Phép toán Ý nghĩa
+ Cộng
- Trừ
* Nhân
/ Chia nguyên
% Chia dư
++ Tăng
-- Giảm
= Gán

Phép toán thao tác bit

Phép toán Ý nghĩa
& AND nhị phân
| OR nhị phân
^ XOR
~ Bù bit
<< Dịch trái
>> Dịch phải
>>> Dịch phải và điền 0 vào bit trống

Phép toán quan hệ

Phép toán Ý nghĩa
== So sánh bằng
!= So sánh khác
> Lớn hơn
>= Lớn hơn hoặc bằng
< Nhỏ hơn
<= Nhỏ hơn hoặc bằng

Phép toán logic

Phép toán Ý nghĩa
&& AND
|| OR
! NOT

Ép kiểu

- Ép kiu rng (widening conversion): tkiu nhsang kiu ln (không mt mát thông tin)

- Ép kiu hp (narrow conversion): tkiu ln sang kiu nh(có khnăng mt mát thông tin):

<tên biến> = (kiểu_dữ_liệu) <tên_biến>;

Ví d:
float fNum = 2.2;
int iCount = (int) fNum; //(iCount = 2)

Điều kiện ba ngôi

Cú pháp:

<điều kiện> ? <biểu thức 1> : < biểu thức 2>

Nếu điu kin đúng thì có giá tr, hay thc hin <biu thc 1>, còn ngược li là <biu thc 2>.

<điu kin>: là mt biu thc logic

<biu thc 1>, <biu thc 2>: có thlà hai giá tr, hai biu thc hoc hai hành động.

Ví d:
int x = 10;
int y = 20;
int Z = (x<y) ? 30 : 40;
//Kết quz = 30 do biu thc (x < y) là đúng.

Thứ tự ưu tiên

Thtự ưu tiên tính ttrái qua phi và ttrên xung dưới.

() [] .  
++ -- ~ !
* / %  
+ -    
>> >>> <<  
> >= < <=
== !=    
&      
^      
|      
&&      
||      
?:      
=      

if-else

Điều kiện if-else dùng trong trường hợp muốn rẽ nhánh chương trình theo điều kiện. Ví dụ, nếu biến nguyên n chia hết cho 2 thì suy ra n là số chẵn, ngược lại n là số lẻ. Có nhiều dạng rẽ nhánh khác nhau, dưới đây ta sẽ tìm hiểu những dạng điều kiện if - else khác nhau để có thể áp dụng vào từng trường hợp cụ thể thực tế.

1. if không có else

Cú pháp:

if(Điều_kiện) {
  Khối_lệnh;
}

Theo cú pháp trên, Khối_lệnh chỉ được thực hiện khi Điều_kiện là đúng, còn không thì bỏ qua khối lệnh. Trong trường hợp khối lệnh chỉ có duy nhất một lệnh thì ta có thể bỏ qua cặp ngoặc xoắn ({}).

Điều_kiện ở đây có thể là một điều kiện đơn hoặc cũng có thể bao gồm nhiều điều kiện sử dụng các phép toán Logic. Ví dụ:

int n=5;
if(n%2==1) { //điều kiện này đúng,
  System.out.printf("%d là số lẻ", n); //nên khối lệnh sẽ được thực hiện.
}

2. if - else thông thường

Cú pháp:

if(Điều_kiện) {
  Khối_lệnh1;
}
else {
  Khối_lệnh2;
}

Theo cú pháp trên, nếu Điều_kiện là đúng thì thực hiện Khối_lệnh1, không đúng thì thực hiện Khối_lệnh2. Ví dụ:

int m=6;
if(m%2 != 0){ //điều kiện này sai,
  System.out.printf("%d là số lẻ", m); //nên khối lệnh này không được thực hiện.
}
else{
  System.out.printf("%d là số chẵn", m); //vậy thì khối lệnh này sẽ được thực hiện.
}

Ví dụ dưới đây sẽ áp dụng if-else để xác định một số N bất kỳ nào đó có phải là số nguyên hay không. Số nguyên là số có phần thập phân bằng 0 hoặc phần nguyên của số đó bằng chính nó, ví dụ như 5.0 được coi là số nguyên vì phần thập phân bằng 0 hay phần nguyên của nó (là 5) bằng chính nó (5.0).

package songuyen;

import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class SoNguyen{

    public static void main(String[] args) {
        float N;
        Scanner sc = new Scanner(System.in);
        System.out.print("Mời bạn nhập 1 số bất kỳ: ");
        N = sc.nextFloat();
        if (N % 1 == 0) { //nếu N chia hết cho 1 (phần dư của N cho 1 bằng 0)
            System.out.printf("%.0f là số nguyên", N); //thì N là số nguyên
        } else { 
            System.out.printf("%f không phải là số nguyên", N);
        }
    }

}

3. Nhiều if-else

Loại điều kiện này được sử dụng trong trường hợp có nhiều điều kiện rẽ nhánh và thường có dạng như sau:

if(Điều_kiện_1){
  Khối_lệnh_1;
}
else if(Điều_kiện_2){
  Khối_lệnh_2;
}
...
else if(Điều_kiện_n){
  Khối_lệnh_n;
}
else{
  Khối_lệnh;
}

Theo cú pháp này, nếu Điều_kiện_i nào đúng thì Khối_lệnh_i của if tương ứng sẽ được thực hiện, tất cả các khối lệnh khác sẽ bị bỏ qua.

Trong trường hợp tất cả các điều kiện đều sai thì Khối_lệnh của else nằm cuối cùng sẽ được thực hiện. Ví dụ sau sẽ xác định loại học lực dựa trên biến diemtrungbinh:

package diemtrungbinh;

import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class DiemTrungBinh {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        float diemtrungbinh;

        System.out.print("\nMời bạn nhập điểm trung bình: ");
        diemtrungbinh = sc.nextFloat();
        if (diemtrungbinh >= 9) {
            System.out.println("Xuất sắc");
        } else if (diemtrungbinh >= 8) {
            System.out.println("Giỏi");
        } else if (diemtrungbinh >= 6.5) {
            System.out.println("Khá");
        } else if (diemtrungbinh >= 5) {
            System.out.println("Trung bình");
        } else { //nếu tất cả các điều kiện trên đều sai
            System.out.println("Học lực yếu"); //thì khối lệnh này sẽ được thực hiện
        }
    }

}

4. if lồng

if lồng có nghĩa là if nằm trong if, hay trong if này lại có if khác. Dưới đây là một dạng if lồng:

if(Điều_kiện){
  if(Điều_kiện_1){
    Khối_lệnh_1;
  }
  if(Điều_kiện_2){
    Khối_lệnh_2;
  }
  else{
    Khối_lệnh_3;
  }
}
else{
  Khối_lênh;
}

Theo dạng trên thì trong if ngoài có 2 if. Vấn đề có thể nảy sinh là phải nhận diện rõ else nào đi với if nào để thiết lập đúng cho các điều kiện rẽ nhánh.

Bạn cần nhớ quy tắc sau:

"else sẽ đi với if gần nó nhất nhưng phải nằm trong cùng một khối lệnh".

Theo dạng trên thì cặp else và if có màu xanh (green) sẽ đi với nhau, cặp else và if màu đỏ (red) sẽ đi với nhau.

Ví dụ, sử dụng ngôn ngữ lập trình C để giải phương trình bậc hai: ax2 + bx + c = 0. Để giải được phương trình bậc hai thì ta cần phải có ba dữ liệu là ba hệ số a, b và c. Sau đây là đoạn mã thực hiện chương trình:

// Bài toán giải phương trình bậc hai: ax2 + bx + c = 0

package ptb2;

import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class PTB2 {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        float diemtrungbinh;
        float a, b, c, delta, x1, x2;
        System.out.print("Nhập hệ số a: "); //trước tiên yêu cầu người dùng nhập vào các hệ số a
        a = sc.nextFloat();
        System.out.print("Nhập hệ số b: "); //hệ số b
        b = sc.nextFloat();
        System.out.print("Nhập hệ số c: "); //và hệ số c
        c = sc.nextFloat();
        if (a == 0) { //nếu a bằng 0 thì
            if (b == 0) { //nếu b bằng 0 thì
                if (c == 0) { //nếu c bằng 0 thì
                    System.out.println("Phương trình vô số nghiệm!"); //in ra phương trình vô số nghiệm
                } else {
                    System.out.println("Phương trình vô nghiệm!"); //nếu không thì in ra phương trình vô nghiệm
                }
            } else { //trường hợp b khác 0
                System.out.printf("Phương trình có một nghiệm, x = %g", -c / b); //thì phương trình có nghiệm -c/b
            }
        } else { //trường hợp a khác 0 thì
            delta = b * b - 4 * a * c; //trước tiên tính biệt thức delta
            if (delta < 0) { //nếu delta âm
                System.out.println("Phương trình vô nghiệm!"); //thì phương trình vô nghiệm
            } else if (delta == 0) { //nếu không thì nếu delta bằng 0
                System.out.printf("Phương trình có nghiệm kép, x1 = x2 = %g", -b / (2 * a)); //thì phương trình có nghiệm kép bằng -b/(2*a)
            } else { //trường hợp còn lại (tức là delta dương)
                System.out.println("Phương trình có hai nghiệm phân biệt:"); //thì khẳng định phương trình có hai nghiệm phân biệt
                x1 = (float) (-b + Math.sqrt(delta)) / (2 * a); //tính nghiệm x1
                x2 = (float) (-b - Math.sqrt(delta)) / (2 * a); //tính nghiệm x2
                System.out.printf("x1 = %f%n", x1); //rồi in ra x1
                System.out.printf("x2 = %f%n", x2); //và x2.
            }
        }
    }

}

Xem thêm

Lớp (Class) và Đối tượng (Object)

Giới thiệu

Lớp là một cấu trúc logic mà định nghĩa khuôn dạng và tính chất của các đối tượng. Vì lớp là đơn vị thực thi chính của lập trình hướng đối tượng trong Java, nên bất kỳ khái niệm nào trong chương trình Java phải được đóng gói trong lớp.

Trong Java, lớp được định nghĩa như là một kiểu dữ liệu mới. Kiểu dữ liệu này được dùng để tạo các đối tượng có kiểu của nó. Mỗi đối tượng được tạo từ lớp sẽ chứa bản sao của chính nó bao gồm các thuộc tính được định nghĩa trong lớp. Các thuộc tính cũng được gọi là các trường và biểu diễn trạng thái của đối tượng. Việc khởi tạo các đối tượng được thực hiện bằng cách sử dụng các hàm tạo và hành vi của các đối tượng được định nghĩa bằng cách sử dụng các phương thức.

Khai báo một lớp

Một khai báo lớp phải được bắt đầu với từ khóa class và theo sau là tên lớp được khai báo.

Bên cạnh đó, những quy ước sau đây ta cần phải chú ý khi đặt tên cho lớp:

- Tên lớp nên là một danh từ.

- Tên lớp có thể được đặt với ký tự đầu tiên của mỗi từ là in hoa.

- Tên lớp nên đơn giản, mang tính mô tả, và đầy đủ ý nghĩa.

- Tên lớp không thể là một từ khóa bất kỳ nào đó của Java.

- Tên lớp không thể bắt đầu là số. Tuy nhiên, có thể bắt đầu với dấu dola ($) hoặc ký tự gạch dưới.

Cú pháp khai báo một lớp trong Java là như sau:

class <tên_lớp> {
  //thân của lớp
}

Việc khai báo lớp được đặt bên trong một khối mã. Nói cách khác, thân của lớp được đặt trong cặp ngoặc xoắn ({}). Trong thân lớp, ta có thể khai báo các thành phần như trường, phương thức, và hàm tạo.

Đoạn mã 1 dưới đây khai báo một lớp có tên Customer.

Đoạn mã 1:

class KhachHang {
  //thân lớp
}

Trong đoạn mã, lớp được khai báo đóng vai trò là một kiểu dữ liệu mới. Tên của kiểu dữ liệu mới là KhachHang. Việc khai báo kiểu dữ liệu này là một mẫu để tạo nhiều đối tượng với các đặc điểm tương tự và không mất vùng nhớ.

Đối tượng

Đối tượng là thể hiện thực sự của lớp. Dưới đây ta sẽ tìm hiểu hai cách khác nhau để tạo một đối tượng.

1. Khai báo và khởi tạo một đối tượng

Một đối tượng được tạo bằng cách sử dụng toán tử new. Khi gặp toán tử new, thì JVM cấp phát vùng nhớ cho đối tượng và trả về một tham chiếu hay địa chỉ vùng nhớ của đối tượng được cấp phát. Tham chiếu hay địa chỉ vùng nhớ sau đó được lưu trong một biến. Biến này được gọi là biến tham chiếu.

Cú pháp để khai báo và khởi tạo một đối tượng là như sau:

<tên_lớp> <tên_biến> = new <tên_lớp>();

trong đó,

new: Là một toán tử mà sẽ cấp phát vùng nhớ của một đối tượng khi chạy chương trình (runtime).

tên_biến: Là biến lưu tham chiếu của đối tượng.

Đoạn mã 2 thể hiện việc tạo một đối tượng trong chương trình Java.

Đoạn mã 2:

KhachHang objKhachHang = new KhachHang();

Ta nhìn vào biểu thức bên phải phép gán, new KhachHang() sẽ cấp phát vùng nhớ khi thực thi chương trình. Sau khi vùng nhớ được cấp phát cho đối tượng, thì nó sẽ trả về tham chiếu hoặc địa chỉ của vùng nhớ của đối tượng đó, và được lưu vào biến tham chiếu có tên objKhachHang nằm bên trái phép gán.

2. Tạo một đối tượng theo quy trình hai bước

Ngoài ra, một đối tượng có thể được tạo bằng cách sử dụng hai bước là tạo một biến tham chiếu rồi cấp phát vùng nhớ động của đối tượng.

Để sử dụng giải pháp này, ta tạo một đối tượng tham chiếu trước, bỏ qua việc sử dụng toán tử new.

Cú pháp khai báo tham chiếu đối tượng như sau:

<tên_lớp> <tên_biến>;

trong đó,

tên_biến: Là một biến mà sẽ không trỏ tới bất kỳ vùng nhớ nào.

Hình dưới đây cho thấy câu lệnh KhachHang objKhachHang; sẽ khai báo một biến tham chiếu.

Java1: Khai báo biến tham chiếu

Mặc định, giá trị null được lưu trữ trong biến tham chiếu của đối tượng. Nói cách khác, có nghĩa là biến tham chiếu lúc này không trỏ tới bất kỳ đối tượng nào. Đối tượng objKhachHang đóng vai trò như một tham chiếu tới một đối tượng có kiểu KhachHang. Nếu objKhachHang được sử dụng tại lần trỏ này, mà không được thể hiện, thì kết quả là trình dịch sẽ báo lỗi.

Vì vậy, Trước khi sử dụng một đối tượng, thì đối tượng phải được khởi tạo bằng cách sử dụng toán tử new. Toán tử new sẽ cấp phát vùng nhớ động cho một đối tượng. Ví dụ, objKhachHang = new KhachHang();. Câu lệnh này sẽ cấp phát vùng nhớ cho đối tượng và địa chỉ vùng nhớ của đối tượng được cấp phát được lưu trong biến objKhachHang.

Hình sau thể hiện việc cấp phát vùng nhớ cho đối tượng và lưu trữ đia chỉ (tham chiếu) của nó vào biến tham chiếu objKhachHang.

Java: Tạo một dối tượng

Xem thêm:

Các thành phần của một lớp

Mỗi một lớp có hai loại thành phần là các trường và các phương thức. Các trường định nghĩa các đặc điểm của một đối tượng đã tạo từ lớp và được gọi là các biến thể hiện. Các phương thức được sử dụng để thực thi các hành vi của các đối tượng và được gọi là các phương thức thể hiện.

Các biến thể hiện

Các trường hay biến được định nghĩa trực tiếp trong một lớp được gọi là các biến thể hiện. Các biến thể hiện được dùng để lưu trữ dữ liệu của lớp. Chúng được gọi là các biến thể hiện bởi vì mỗi thể hiện của lớp, đó là đối tượng của lớp sẽ có bản sao của chính nó về các biến thể hiện. Điều này có nghĩa là mỗi đối tượng của lớp sẽ chứa các biến thể hiện trong quá trình tạo.

Xét một kịch bản trong đó lớp KhachHang biểu diễn các thông tin chi tiết của những khách hàng đang có tài khoản tại ngân hàng. Trong kịch bản này, một câu hỏi điển hình mà có thể được hỏi là 'Những dữ liệu khác nhau nào được yêu cầu để xác định một khách hàng trong lĩnh vực ngân hàng và biểu diễn nó như là một đối tượng đơn?'.

Dữ liệu nhận diện cần có của một khách hàng ngân hàng gồm: ID, Tên, Địa chỉ và Tuổi. Để ánh xạ tới các dữ liệu này trong lớp KhachHang, thì các biến thể hiện cần phải được tạo. Mỗi đối tượng được tạo ra từ lớp KhachHang sẽ có bản copy các biến thể hiện của chính nó.

Mỗi thể hiện của lớp có các biến thể hiện của chính nó và được khởi tạo với một giá trị duy nhất. Mỗi sự thay đổi được thực hiện trên biến thể hiện của một đối tượng sẽ không ảnh hưởng đến các biến thể hiện của đối tượng khác.

Cú pháp khai báo một biến thể hiện trong một lớp là như sau:

[bổ_từ_truy_cập] kiểu_dữ_liệu tên_biến_thể_hiện;

trong đó,

bổ_từ_truy_cập: Là một từ khóa tùy chọn xác định mức độ truy cập của một biến thể hiện, nó có thể là private, protected, hoặc public.

kiểu_dữ_liệu: Xác định kiểu dữ liệu của biến.

tên_biến_thể_hiện: Xác định tên của biến.

Các biến thể hiện được khai báo theo cách tương tự với các biến cục bộ, nhưng phải nằm bên ngoài bất kỳ một định nghĩa phương thức nào. Chúng chỉ có thể được truy cập thông qua các đối tượng bằng cách sử dụng toán tử dấu chấm (.).

Đoạn mã 1 thể hiện việc khai báo các biến thể hiện của một lớp trong chương trình Java.

Đoạn mã 1:

1: public class KhachHang{
2: // Khai báo các biến thể hiện
3: int IDKhach;
4: String tenKhach;
5: String diaChiKhach;
6: int tuoiKhach;
7: /* Vì phương thức main() là một thành phần của lớp, nên nó có thể
8: truy cập các thành phần khác của lớp */
9: public static void main(String[] args) {
10: // Khai báo và khởi tạo một đối tượng có kiểu KhachHang
11: KhachHang objKhach = new KhachHang();
12: // Truy cập các biến thể hiện của đối tượng objKhach
13: objKhach.IDKhach = 81;
14: objKhach.tenKhach = "Long";
15: objKhach.diaChiKhach = "Ha Noi";
16: objKhach.tuoiKhach = 29;
17: // Hiển thị thông tin của đối tượng objKhach
18: System.out.println("Mã khách: " + objKhach.IDKhach);
19: System.out.println("Tên khách: " + objKhach.tenKhach);
20: System.out.println("Địa chỉ: " + objKhach.diaChiKhach);
21: System.out.println("Tuổi: " + objKhach.tuoiKhach);
22: }
23: }

Trong đoạn mã trên, dòng từ 3 đến 6 khai báo các biến thể hiện. Dòng 11 tạo một đối tượng kiểu KhachHang và lưu tham chiếu của nó vào biến objKhach. Các dòng 13 tới 16 truy cập vào các biến thể hiện và gán giá trị cho chúng. Lưu ý rằng để gán giá trị cho biến thể hiện thì ta ghép tên biến với tên của thể hiện và đặt toán tử dấu chấm (.) ở giữa.

Cuối cùng, các dòng 18 đến 21 hiển thị các giá trị được gán cho các biến thể hiện của đối tượng objKhach.

Các phương thức thể hiện

Các phương thức thể hiện là các hàm được khai báo trong một lớp và được sử dụng để giải quyết các hoạt động trên các biến thể hiện. Phương thức thể hiện thực thi hành vi của đối tượng. Nó có thể được truy cập bằng cách tạo một đối tượng của lớp trong đó nó được định nghĩa và sau đó gọi đến phương thức. Ví dụ, lớp Oto có thể có một phương thức là hamPhanh() thể hiện hành vi 'Hãm phanh'. Để giải quyết hành động hãm phanh, hamPhanh() sẽ phải được gọi bởi một đối tượng của lớp Oto.

Phương thức thể hiện có thể truy cập các biến thể hiện được khai báo trong lớp và thao tác dữ liệu trong trường.

Những quy ước sau đây cần phải được tuân theo trong khi đặt tên cho một phương thức:

- Không thể là từ khóa Java.

- Không thể chứa dấu cách.

- Không thể bắt đầu bằng số.

- Có thể bắt đầu bằng ký tự thường, ký tự gạch dưới, hoặc ký tự '$'.

- Nên là một động từ viết bằng chữ thường.

- Nên có tính mô tả và có ý nghĩa.

- Tên nên có nhiều từ với từ đầu tiên là một động từ viết bằng chữ thường, các từ sau là tính từ, danh từ, ... và ký tự đầu tiên của mỗi từ sau là chữ in hoa. Ví dụ như hamPhanh(), tangToc().

Cú pháp định nghĩa một phương thức thể hiện trong một lớp là như sau:

[bổ_từ_truy_cập] <kiểu_trả_về> <tên_phương_thức> ([danh sách đối số]) {
  // Thân của phương thức
}

trong đó,

bổ_từ_truy_cập: Xác định mức truy cập của một phương thức thể hiện.

kiểu_trả_về: Xác định kiểu dữ liệu của giá trị mà được trả về bởi phương thức.

tên_phương_thức: Là tên phương thức.

danh sách đối số: Là các giá trị truyền tới phương thức.

Mỗi thể hiện của lớp có các biến thể hiện của chính nó, nhưng các phương thức thể hiện được chia sẻ bởi tất cả các thể hiện của lớp trong quá trình thực thi.

Đoạn mã 2 thể hiện cách định nghĩa các phương thức thể hiện của lớp KhachHang.

Đoạn mã 2:

public class KhachHang {
  //Khai báo các biến thành phần tại đây ...
  /**
  * Tạo phương thức thể hiện có tên suaDiaChiKhach
  * để thay đổi địa chỉ của đối tượng khách hàng
  */

  void suaDiaChiKhach(String diaChi) {
    diaChiKhach = diaChi;
  }
  /**
  * Tạo một phương thức thể hiện có tên thongTinKhach
  * để hiển thị thông tin chi tiết của đối tượng khách hàng
  */

  void thongTinKhach() {
    System.out.println("So dinh danh khach hang: " + IDKhach);
    System.out.println("Ten khach hàng: " + tenKhach);
    System.out.println("Dia chi khach hang: " + diaChiKhach);
    System.out.println("Tuoi khach hang: " + tuoiKhach);
  }
}

Lớp KhachHang ở đoạn mã 1 được sửa đổi với việc thêm các phương thức có tên là suaDiaChiKhach()thongTinKhach(). Phương thức suaDiaChiKhach() sẽ truy cập một giá trị chuỗi thông qua tham số địa chỉ. Sau đó nó gán giá trị của đối số diaChi cho trường diaChiKhach. Vì các phương thức là một phần của khai báo lớp, nên chúng có thể truy cập các thành phần khác của lớp, chẳng hạn như các biến thể hiện và các phương thư của lớp.

Tương tự như vậy, phương thức thongTinKhach() hiển thị các chi tiết của đối tượng khách hàng.

Lưu ý rằng khi lớp KhachHang được biên dịch thì trình biên dịch sẽ đặt nó trong một tập tin có tên là KhachHang.class (tập tin bytecode). Lớp này không thể được thực thi vì không có sự hiện diện của phương thức main() trong nó.

Gọi một phương thức

Ta có thể truy cập một phương thức của lớp bằng cách tạo một đối tượng của lớp. Để gọi một phương thức, ta sử dụng tên đối tượng, theo sau là toán tử dấu chấm (.) và sau đó là tên phương thức muốn gọi.

Trong Java, một phương thức luôn luôn được gọi từ một phương thức khác. Phương thức gọi phương thức khác thì được gọi là phương thức calling. Phương thức được gọi thì gọi là phương thức called. Sau khi thực thi tất cả các câu lệnh trong khối lệnh của phương thức called, thì quyền điều khiển sẽ được trả lại cho phương thức calling.

Phần lớn các phương thức được gọi từ phương thức main() của lớp, đó là đầu vào của việc thực thi chương trình.

Đoạn mã 3 dưới đây trình bày một lớp, trong đó phương thức main() tạo một thể hiện của lớp KhachHang và gọi tới các phương thức được định nghĩa trong lớp này.

Đoạn mã 3:

public class TestKhachHang {
  /**
  * Phương thức main() tạo thể hiện của lớp
  * KhachHang và gọi tới những phương thức của nó
  */

  public static void main(String[] args) {
    // Tạo một đối tượng của lớp KhachHang
    KhachHang objKhach = new KhachHang();
    // Khởi tạo giá trị cho đối tượng
    objKhach.IDKhach= 100;
    objKhach.tenKhach = "Phuong";
    objKhach.diaChiKhach = "Quang Trung, Ha Dong";
    objKhach.tuoiKhach = 21;
    /* Gọi phương thức thể hiện để hiển thị các chi tiết của đối tượng objKhach */
    objKhach.thongTinKhach();
    /* Gọi phương thức thể hiện để thay đổi địa chỉ của đối tượng objKhach */
    objKhach.suaDiaChiKhach("Thanh Xuan, Ha Noi");
    /* Gọi phương thức thể hiện sau khi thay đổi địa chỉ của đối tượng objKhach */
    objKhach.thongTinKhach();
  }
}

Đoạn mã cho thấy một đối tượng objKhach có kiểu lớp KhachHang và khởi tạo các biến thể hiện của nó. Phương thức thongTinKhach() được gọi bằng cách sử dụng đối tượng objKhach. Phương thức này hiển thị giá trị của các biến thể hiện đã được khởi tạo giá trị ra màn hình.

Sau đó, phương thức suaDiaChiKhach("Thanh Xuan, Ha Noi") được gọi để thay đổi dữ liệu của trường diaChiKhach. Cuối cùng, phương thức thongTinKhach() lại được gọi để hiển thị các thông tin của đối tượng objKhach.

Hàm tạo (Constructor)

Tổng quan

Mỗi lớp có thể chứa nhiều biến trong đó việc khai báo và khởi tạo sẽ trở nên khó khăn để theo dõi nếu chúng được thực hiện bên trong các khối khác nhau. Tương tự như vậy, có thể có những hoạt động khởi đầu khác mà cần được giải quyết trong một ứng dụng, chẳng hạn như mở một tập tin. Java cho phép các đối tượng khởi tạo chính bản thân chúng một cách trực tiếp ngay trong quá trình tạo chúng. Điều này được được thực hiện bằng cách định nghĩa các hàm tạo (constructor) trong lớp.

Mỗi hàm tạo là một phương thức có tên giống với tên của lớp chứa nó. Hàm tạo có nhiệm vụ khởi tạo giá trị cho các biến lớp hoặc thực hiện các hoạt động khởi tạo chỉ một lần trong khi tạo một đối tượng của lớp. Các hàm tạo sẽ tự động được gọi và thực thi mỗi khi một thể hiện của lớp được tạo.

Chú ý: Hàm tạo không có kiểu trả về. Điều này là bởi vì kiểu trả về ngầm định của hàm tạo chính là bản thân lớp chứa nó. Bạn cũng có quyền áp dụng hình thức tải chồng phương thức đối với hàm tạo.

Cú pháp khai báo hàm tạo của một lớp là như sau:

<tên_lớp>() {
  // Các câu lệnh khởi tạo
}

Đoạn mã 1 dưới đây định nghĩa một lớp có tên là ChuNhat và có một hàm tạo.

Đoạn mã 1:

public class ChuNhat {
  int rong;
  int cao;
  ChuNhat() { /* Hàm tạo không tham số cho lớp ChuNhat */
    System.out.println("Tôi-Hàm tạo không đối số được gọi ...");
    rong = 10;
    cao = 10;
  }
}

Đoạn mã khai báo một phương thức có tên ChuNhat() và đây chính là hàm tạo. Phương thức này được gọi bởi Máy ảo Java (JVM) để khởi tạo hai biến thể hiện của lớp ChuNhat gồm rong cao khi đối tượng của lớp được tạo. Hàm tạo này không có bất kỳ tham số nào, vì thế nó được gọi là hàm tạo không đối số hay không tham số.

Lời gọi tới hàm tạo

Hàm tạo được gọi trực tiếp trong quá trình tạo đối tượng. Có nghĩa là mỗi khi toán tử new xuất hiện thì bộ nhớ sẽ được cấp phát cho đối tượng. Phương thức tạo, nếu được cung cấp trong lớp thì được gọi bởi JVM để khởi tạo đối tượng. Ví dụ:

ChuNhat objChuNhat = new ChuNhat();

Cặp ngoặc tròn đặt sau tên lớp trong câu lệnh trên chỉ ra rằng đó là lời gọi tới hàm tạo.

Đoạn mã 2 trình bày mã lệnh để gọi hàm tạo cho lớp ChuNhat.

Đoạn mã 2:

public class TestChuNhat {
  public static void main(String[] args) {
    //khởi tạo một đối tượng của lớp ChuNhat
    ChuNhat objChuNhat = new ChuNhat();
    //truy cập các biến thể hiện sử dụng tham chiếu đối tượng
    System.out.println("Rộng: " + objChuNhat.rong);
    System.out.println("Cao: " + objChuNhat.cao);
  }
}

Đoạn mã trên tạo một đối tượng có tên objChuNhat. Trước tiên đối tượng của lớp ChuNhat được cấp phát vùng nhớ và sau đó hàm tạo được gọi. Hàm tạo khởi tạo các biến thể hiện của đối tượng mới là rongcao đều cùng giá trị là 10.

Hàm tạo mặc định

Trong trường hợp bạn không định nghĩa hàm tạo nào trong lớp, thì JVM sẽ gọi tới hàm tạo ngầm định (được trình biên dịch cung cấp) để khởi tạo các đối tượng. Hàm tạo ngầm định này không có đối số và nó sẽ khởi tạo giá trị cho các biến thể hiện của đối tượng mới các giá trị mặc định theo kiểu dữ liệu của từng biến.

Đoạn mã 3 dưới đây tạo lớp NhanVien nhưng không định nghĩa hàm tạo nào.

Đoạn mã 3:

public class NhanVien {
  // Khai báo các biến thể hiện
  String tenNhanVien;
  int tuoiNhanVien;
  double luongNhanVien;
  boolean tinhTrangHonNhan;
  /* Phương thức để hiển thị thông tin chi tiết nhân viên */
  void chiTietNhanVien() {
    System.out.println("Chi tiết nhân viên");
    System.out.println("================");
    System.out.println("Tên: " + tenNhanVien);
    System.out.println("Tuổi: " + tuoiNhanVien);
    System.out.println("Lương: " + luongNhanVien);
    System.out.println("Tình trạng hôn nhân: " + tinhTrangHonNhan);
  }
}

Đoạn mã trên khai báo một lớp NhanVien với các biến thể hiện và một phương thức thể hiện là chiTietNhanVien(). Phương thức này in giá trị của các biến thể hiện ra màn hình.

Đoạn mã 4 dưới đây tạo một lớp có chứa phương thức main() trong đó tạo một thể hiện của lớp NhanVien và từ đó gọi các phương thức của nó.

Đoạn mã 4:

public class TestNhanVien {
  public static void main(String[] args) {
    NhanVien objNV = new NhanVien();
    objNV.chiTietNhanVien();
  }
}

Vì lớp NhanVien không có bất kỳ hàm tạo nào được định nghĩa, nên hàm tạo mặc định sẽ được tạo khi thực thi chương trình.

Khi câu lệnh new NhanVien() được thực thi, thì đối tượng được cấp phát vùng nhớ và các biến thể hiện được hàm tạo mặc định khởi tạo các giá trị ứng với từng kiểu dữ liệu. Sau đó, phương thức chiTietNhanVien() được thực thi để hiển thị các giá trị của các biến thể hiện của đối tượng objNV.

Bảng dưới đây liệt kê các giá trị mặc định của từng kiểu dữ liệu sẽ được gán cho biến thể hiện của lớp.

Kiểu dữ liệu Giá trị mặc định
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0
char '\u0000'
boolean False
String null

Hàm tạo có tham số

Trong đoạn mã 1 bên trên có định nghĩa hàm tạo không đối số cho lớp ChuNhat, trong đó gán giá trị 10 cho các biến thể hiện rongcao. Và điều này cũng có nghĩa là tất cả các đối tượng của lớp ChuNhat đều sẽ được khởi tạo các giá trị giống nhau cho các biến thể hiện. Điều này không hữu dụng trong nhiều tình huống.

Để khắc phục điều này ta có thể định nghĩa hàm tạo chứa một danh sách các tham số với mục đích khởi tạo giá trị cho các biến thể hiện của đối tượng tương ứng. Các giá trị khởi tạo sẽ được truyền trong quá trình tạo đối tượng.

Đoạn mã 3 sau đây sẽ định nghĩa thêm hàm tạo hai tham số cho lớp ChuNhat.

Đoạn mã 3:

public class ChuNhat {
  ...........
  ChuNhat(int rong, int cao) {
    System.out.println("Hàm tạo hai tham số");
    this.rong = rong;
    this.cao = cao;
  }
  void hienThiCacChieu() {
    System.out.println("Rộng: " + rong);
    System.out.println("Cao: " + cao);
  }
}

Đoạn mã trên khai báo một hàm tạo có đối số là ChuNhat(int rong, int cao). Trong quá trình thực thi, hàm tạo sẽ chấp nhận giá trị của hai tham số và gán chúng cho các biến rong và cao tương ứng của đối tượng.

Trong đoạn mã 4 dưới đây có hai câu lệnh tạo hai đối tượng objCN1objCN2, chiều rộng và chiều cao của hai đối tượng này được khởi tạo với việc gọi đến hàm tạo hai đối số.

Đoạn mã 4:

public class TheHienChuNhat {
  public static void main(String[] args) {
    //khai báo và khởi tạo hai đối tượng cho lớp ChuNhat
    ChuNhat objCN1 = new ChuNhat(10, 20);
    ChuNhat objCN2 = new ChuNhat(6, 9);
    //gọi phương thức hienThiCacChieu() để hiển thị các giá trị
    System.out.println("\nHình chữ nhật thứ nhất:");
    System.out.println("===================");
    objCN1.hienThiCacChieu();
    System.out.println("\nHình chữ nhật thứ hai:");
    System.out.println("===================");
    objCN2.hienThiCacChieu();
  }
}

Đoạn mã trên tạo hai đối tượng trong đó gọi đến hàm tạo có tham số. Ví dụ, câu lệnh ChuNhat objCN1 = new ChuNhat(10, 20); sẽ tạo một đối tượng. Trong quá trình tạo, những điều sau đây xảy ra theo trình tự:

  1. Việc cấp phát bộ nhớ được thực hiện cho thể hiện mới của lớp.
  2. Các giá trị 1020 được truyền tới hàm tạo có tham số là ChuNhat(int rong, int cao) trong đó khởi tạo giá trị cho các biến thể hiện của đối tượng là rongcao.
  3. Cuối cùng, tham chiếu của thể hiện mới được tạo được trả lại và lưu trong đối tượng objCN1.

Xem thêm:

Bộ khởi tạo cho đối tượng

Các bộ khởi tạo đối tượng trong Java cung cấp một cách tạo một đối tượng và khởi tạo các trường của nó. Với phương pháp tiếp cận thông thường, bạn gọi một hàm tạo để khởi tạo đối tượng, nhưng việc sử dụng bộ khởi tạo đối tượng bạn có thể bổ sung cho việc sử dụng hàm tạo.

Có hai cách khởi tạo các trường hay biến thể hiện của các đối tượng mới được tạo là: sử dụng Bộ khởi tạo biến thể hiện và sử dụng Khối khởi tạo.

1. Bộ khởi tạo biến thể hiện

Trong phương pháp này, ta xác định các tên của các trường muốn được khởi tạo, và cung cấp giá trị khởi tạo cho từng trường đó.

Đoạn mã 1 cho thấy việc khai báo lớp có tên Person và khởi tạo cho các trường nameage của nó.

Đoạn mã 1:

public class Person {
  private String name = "Hieu";
  private int age = 16;
  /**
  * Hiển thị các chi tiết về đối tượng Person
  */

  void displayDetails() {
    System.out.println("Thông tin chi tiết");
    System.out.println("==============");
    System.out.println("Ten: " + name);
    System.out.println("Tuoi: " + age);
  }
}

Trong đoạn mã trên, các biến thể hiện là name và age được khởi tạo các giá trị tương ứng là "John" và 12. Việc khởi tạo các biến khai báo trong lớp không yêu cầu chúng khởi tạo trong một hàm tạo.

Đoạn mã 2 cung cấp lớp chứa hàm main() truy cập các đối tượng kiểu Person.

Đoạn mã 2:

public class TestPerson {
  /**
  * @param args the command line arguments
  */

  public static void main(String[] args) {
    Person objPerson1 = new Person();
    objPerson1.displayDetails();
  }
}

Đoạn mã khai báo một đối tượng có kiểu Person và gọi phương thức để hiển thị các chi tiết.

Hình dưới đây thể hiện kết quả của đoạn mã 1 và đoạn mã 2 khi áp dụng bộ khởi tạo biến thể hiện:

java1-bo-khoi-tao-bien-the-hien

2. Khối khởi tạo

Với phương pháp này, một khối khởi tạo được xác định bên trong lớp. Khối khởi tạo được thực thi trước khi thực thi hàm tạo trong quá trình khởi tạo một đối tượng.

Đoạn mã 3 thể hiện lớp Account với một khối khởi tạo.

Đoạn mã 3:

public class Account {
  private int accountID;
  private String holderName;
  private String accountType;
  /**
  * Khối khởi tạo là một cặp ngoặc {} và bên trong chứa các câu lệnh khởi tạo cho các biến thể hiện
  */

  {
    accountID = 100;
    holderName = "Nguyen Van Minh";
    accountType = "Tai khoan tiet kiem";
  }
  /**
  * Hiển thị các chi tiết của đối tượng Account
  */
  public void displayAccountDetails() {
    System.out.println("Thông tin chi tiết");
    System.out.println("===============");
    System.out.println("Account ID: " + accountID + "\nAccount Type: " + accountType);
  }
}

Trong đoạn mã, khối khởi tạo tiến hành khởi tạo các biến thể hiện của lớp. Khối khởi tạo về cơ bản được sử dụng để giải quyết những chuỗi khởi tạo phức tạp.

Đoạn mã 4 thể hiện đoạn mã trong phương thức main() để khởi tạo đối tượng Account thông qua khối khởi tạo.

Đoạn mã 4:

public class TestInitializationBlock {
  public static void main(String[] args) {
    Account objAccount = new Account();
    objAccount.displayAccountDetails();
  }
}

Kết quả của đoạn mã 3 và 4 được thể hiện ở hình sau:

java1-khoi-khoi-tao

Từ khóa this

Java cung cấp từ khóa 'this' cho phép ta có thể sử dụng trong phương thức thể hiện hoặc hàm tạo để tham chiếu tới đối tượng hiện thời, đó là đối tượng mà trong đó phương thức hay hàm tạo được gọi. Bất kỳ một thành phần nào của đối tượng hiện thời thì đều có thể được tham chiếu từ bên trong một phương thức thể hiện hoặc một hàm tạo bằng cách sử dụng từ khóa 'this'.

Từ khóa this được sử dụng một cách ngầm định trong khi tham chiếu tới các biến và phương thức của lớp.

Ví dụ, xét phương thức calcArea() trong đoạn mã dưới đây.

public class Circle {
  float area; //biến lưu trữ diện tích hình tròn
  public float getPI() {
    return 3.14;
  }
  public void calcArea(int rad) {
    this.area = getPI() * rad * rad;
  }
}

Phương thức calcArea() tính diện tích của hình tròn và lưu vào biến area. Nó truy xuất giá trị của PI bằng cách gọi phương thức getPI(). Ở đây, phương thức gọi không liên quan đến bất kỳ đối tượng nào mặc dù getPI() là một phương thức thể hiện. Đó là bởi vì từ khóa 'this' được sử dụng ngầm định.

Phương thức calcArea() cũng có thể được viết như sau:

public class Circle {
  float area;
  public float getPI(){
    return 3.14;
  }
  public void calcArea(int rad) {
    this.area = this.getPI() * rad * rad;
  }
}

Lưu ý rằng ta sử dụng cách này để chỉ ra đối tượng hiện thời.

Từ khóa 'this' còn có thể được sử dụng để gọi tới hàm tạo từ bên trong một hàm tạo khác.

Điều này còn được gọi là lời gọi hàm tạo tường minh như đoạn mã dưới đây:

public class Circle {
  private float rad; //bán kính
  private float PI; //số PI
  public Circle(){
    PI = 3.14;
  }
  public Circle(float r) {
    this(); //gọi tới hàm tạo không tham số
    rad = r;
  }
}

Từ khóa 'this' có thể được sử dụng để giải quyết vấn đề xung đột tên khi tên của tham số hình thức giống với tên của biến lớp như đoạn mã sau:

public class Circle {
  private float rad; // dòng 1
  private float PI;
  public Circle() {
    PI = 3.14;
  }
  public Circle(float rad) { // dòng 2
    this();
    this.rad = rad; // dòng 3
  }
}

Đoạn mã trên định nghĩa hàm tạo Circle với một đối số có tên là rad ở dòng 2. Như vậy thì đối số này có tên trung với tên biến được khai báo ở dòng 1, mà giá trị sẽ được gán thi thực thi chương trình. Bây giờ, khi gán một giá trị cho biến rad của hàm tạo, thì người dùng sẽ phải viết là rad = rad. Tuy nhiên, trình dịch sẽ hiểu là hai biến rad này thực tế là một. Để giải quyết vấn đề này thì ta cần viết lại là this.rad = rad.

Từ khóa static

Từ khóa static trong Java được sử dụng với mục đích chính là để quản trị bộ nhớ. Chúng ta có thể áp dụng từ khóa static với biến (cũng được gọi là biến lớp, biến class), phương thức (cũng được gọi là phương thức lớp), khối, các lớp được lặp. Từ khóa static thuộc về lớp chứ không thuộc về instance (thể hiện) của lớp.

Biến static trong Java

Khi bạn khai báo một biến là static, thì biến đó được gọi là biến tĩnh, hay biến static.

Biến static có thể được sử dụng để tham chiếu thuộc tính chung của tất cả đối tượng (mà không là duy nhất cho mỗi đối tượng), ví dụ như tên công ty của nhân viên, tên trường học của các sinh viên, …

Biến static lấy bộ nhớ chỉ một lần trong lớp tại thời gian tải lớp đó.

Lợi thế của biến static

Biến static giúp bộ nhớ chương trình của bạn được sử dụng hiệu quả hơn (tiết kiệm bộ nhớ).

Tìm hiểu vấn đề xảy ra khi không có biến static

class Student{ 
     int rollno; 
     String name; 
     String college="V1Study"; 
}  

Giả sử có 500 sinh viên trong trường đại học, bây giờ instance của các thành viên dữ liệu sẽ lấy bộ nhớ mỗi khi đối tượng được tạo. Tất cả sinh viên có rollno và name duy nhất vì thế instance của thành viên dữ liệu là tốt. Ở đây, college là thuộc tính chung của tất cả đối tượng. Nếu chúng ta tạo nó là static, thì trường này sẽ chỉ lấy bộ nhớ một lần.

Ghi chú: Thuộc tính static trong Java được chia sẻ tới tất cả đối tượng.

Ví dụ về biến static trong Java

//Chuong trinh vi du ve bien static trong Java
class Student8{
   int rollno; 
   String name; 
   static String college ="V1Study";  
   Student8(int r,String n){ 
   rollno = r; 
   name = n; 
   }
void display (){System.out.println(rollno+" "+name+" "+college);}  
 public static void main(String args[]){ 
Student8 s1 = new Student8(111,"Hoang"); 
Student8 s2 = new Student8(222,"Thanh");  
 s1.display(); 
s2.display(); 
}
}

Chương trình counter không sử dụng biến static

Trong ví dụ sau, chúng ta tạo một biến instance có tên count được tăng lên trong constructor. Khi biến instance này lấy bộ nhớ tại thời điểm tạo đối tượng, mỗi đối tượng sẽ có bản sao của biến instance đó, nếu nó được tăng lên, nó sẽ không phản ánh các đối tượng khác. Vì thế mỗi đối tượng sẽ có giá trị 1 trong biến count.

class Counter{ 
int count=0; //se lay bo nho (memory) khi bien instance duoc tao 
Counter(){ 
count++; 
System.out.println(count); 
}  
public static void main(String args[]){  
Counter c1=new Counter(); 
Counter c2=new Counter(); 
Counter c3=new Counter();  
 } 
}  

Chương trình counter với biến static trong Java

Như bạn đã thấy ở trên, biến static sẽ lấy bộ nhớ chỉ một lần, nếu bất cứ đối tượng nào thay đổi giá trị của biến static, nó sẽ vẫn ghi nhớ giá trị của nó.

class Counter2{ 
static int count=0; //se lay bo nho chi mot lan và giu lai gia tri cua no 
Counter2(){ 
count++; 
System.out.println(count); 
}  
public static void main(String args[]){  
Counter2 c1=new Counter2(); 
Counter2 c2=new Counter2(); 
Counter2 c3=new Counter2();  
 } 
}   

Phương thức static trong Java

Nếu bạn áp dụng từ khóa static với bất cứ phương thức nào, thì phương thức đó được gọi là phương thức static.

  • Một phương thức static thuộc lớp chứ không phải đối tượng của lớp.

  • Một phương thức static có thể được triệu hồi mà không cần tạo một instance của một lớp.

  • Phương thức static có thể truy cập thành viên dữ liệu static và có thể thay đổi giá trị của nó.

Ví dụ về phương thức static trong Java

//Chuong trinh thay doi thuoc tinh chung cua tat ca doi tuong (truong static). 
class Student9{ 
     int rollno; 
     String name; 
     static String college = "V1Study";  
     static void change(){ 
     college = "QuocGia"; 
     }  
     Student9(int r, String n){ 
     rollno = r; 
     name = n; 
     }  
     void display (){System.out.println(rollno+" "+name+" "+college);}  
    public static void main(String args[]){ 
    Student9.change();  
    Student9 s1 = new Student9 (111,"Hoang"); 
    Student9 s2 = new Student9 (222,"Thanh"); 
    Student9 s3 = new Student9 (333,"Nam");  
    s1.display(); 
    s2.display(); 
    s3.display(); 
    } 
}   

Ví dụ khác về phương thức static mà thực hiện phép tính toán thông thường

//Chuong trinh lay cube (gia tri lap phuong) cua so da cho boi phuong thuc static 
class Calculate{ 
  static int cube(int x){ 
  return x*x*x; 
  }  
  public static void main(String args[]){ 
  int result=Calculate.cube(5); 
  System.out.println(result); 
  } 
}  

Một số hạn chế cho phương thức static

Có hai hạn chế chính cho phương thức static. Đó là:

  • Phương thức static không thể sử dụng thành viên dữ liệu non-static hoặc gọi trực tiếp phương thức non-static.

  • Từ khóa this và super không thể được sử dụng trong ngữ cảnh static.

class A{ 
int a=40;//non static 
 public static void main(String args[]){ 
  System.out.println(a); 
} 
}       

Chạy chương trình trên sẽ cho kết quả là Compile Time Error.

Câu hỏi: Tại sao phương thức main trong Java là static?

Bởi vì đối tượng là không cần thiết để gọi phương thức static nếu nó là phương thức non-static, JVM đầu tiên tạo đối tượng và sau đó gọi phương thức main() mà có thể gây ra vấn đề về cấp phát bộ nhớ bộ nhớ phụ.

Khối static trong Java

Được sử dụng để khởi tạo thành viên dữ liệu static. Nó được thực thi trước phương thức main tại thời gian tải lớp. Dưới đây là ví dụ về khối static trong Java:

class A2{ 
  static{System.out.println("Khoi static duoc trieu hoi");} 
  public static void main(String args[]){ 
   System.out.println("Hello main"); 
  } 
}      

Câu hỏi: Chúng ta có thể thực thi một chương trình mà không có phương thức main()?

Có, một trong các cách đó là khối static trong phiên bản trước của JDK, không trong JDK 1.7.

class A3{ 
  static{ 
  System.out.println("Khoi static duoc trieu hoi"); 
  System.exit(0); 
  } 
}

Từ khóa super

Truy cập các thành phần của lớp cha

Nếu phương thức của lớp con ghi đè phương thức của lớp cha thì ta có thế gọi phương thức bị ghi đè đó của lớp cha bằng cách sử dụng từ khóa super. Ta cũng có thể sử dụng từ khóa super để tham chiếu tới một trường ẩn nào đó (mặc dù trường ẩn là không khuyến khích). Xét lớp có tên Superclass như sau:

public class Superclass {

    public void printMethod() {

        System.out.println("Đang ở lớp Superclass.");

    }

}

Còn dưới đây là lớp con của nó có tên Subclass, lớp con này ghi đè phương thức printMethod():

public class Subclass extends Superclass {

    // ghi đè phương thức printMethod của lớp cha Superclass

    public void printMethod() {
        super.printMethod(); //dùng super để gọi tới phương thức bị ghi đè ở lớp cha
        System.out.println("Đang ở lớp Subclass");
    }

    public static void main(String[] args) {
        Subclass s = new Subclass();
        s.printMethod();   
    }
}

Trong ví dụ trên, trong lớp Subclass, phương thức printMethod() đã ghi đè phương thức tương ứng được định nghĩa ở lớp cha. Vì thế mà nếu bạn muốn sử dụng phương thức printMethod() của lớp cha thì bạn sử dụng super như sau: super.printMethod(). Output của đoạn mã trên như sau:

Đang ở lớp Superclass

Đang ở lớp Subclass

Truy cập hàm tạo của lớp cha từ lớp con

Để truy cập đến hàm tạo của lớp cha thì từ hàm tạo của lớp con ta sử dụng cú pháp như sau:

super(); //gọi hàm tạo không tham số của lớp cha

hoặc:

super(parameter list); //gọi tới hàm tạo có tham số của lớp cha

Lưu ý: Nếu hàm tạo không gọi tường minh tới hàm tạo của lớp cha thì trình biên dịch Java sẽ tự động chèn một lời gọi tới hàm tạo không đối số của lớp cha. Lúc này, nếu lớp cha mà không có hàm tạo không tham số thì bạn sẽ nhận được một lỗi.

Thừa kế (Inheritant)

Giới thiệu

Giả sử có một sinh viên đang phát triển một website với mục đích đưa ra các thói quen ăn uống của các loài động vật khác nhau trên trái đất. Trong khi thiết kế và phát triển website này thì sinh viên nhận ra rằng có nhiều loài vật ăn cùng một loại thức ăn và có tập tính tương tự nhau. Các nhóm con của tất cả các loài vật mà ăn thực vật thì gọi là động vật ăn cỏ, còn động vật ăn động vật thì gọi là động vật ăn thịt, còn nhóm động vật ăn cả thực vật và thịt thì gọi là động vật ăn tạp. Những nhóm hay phân loại như vậy thì được gọi là phân lớp và những nhóm con được gọi là lớp con.

Mối quan hệ is-a của các thực thể trong thế giới thực

Sơ đồ trên thể hiện mối quan hệ giữa các loại đối tượng khác nhau. Ví dụ, Hươu là động vật ăn cỏ và động vật ăn cỏ là động vật. Những thuộc tính chung của tất cả các loài động vật ăn cỏ có thể được lưu trữ trong lớp động vật ăn cỏ. Tương tự như vậy, các thuộc tính chung của tất cả các loài động vật như động vật ăn cỏ, ăn thịt hay ăn tạp đều có thể lưu trữ trong lớp động vật.

Như vậy, lớp Động vật trở thành lớp đầu tiên so với các lớp khác, và các lớp động vật nhỏ hơn là Ăn cỏ, Ăn thịt, và Ăn tạp sẽ thừa kế các thuộc tính và hành vi từ lớp này. Rồi đến những lớp nhỏ hơn nữa như Hươu, Sư tử, Mèo cũng lại thừa kế các thuộc tính và phương thức từ những lớp nhỏ này.

Tương tự như vậy, Java cung cấp khái niệm Thừa kế (Inheritant) để cho phép tạo các lớp con của một lớp.

Đặc điểm và thuật ngữ

Trong Java, khi thực thi thừa kế, lớp được dẫn xuất từ lớp khác được gọi là phân lớp, lớp dẫn xuất, lớp con, hoặc lớp mở rộng. Lớp mà từ đó phân lớp được dẫn xuất thì được gọi là siêu lớp, lớp cơ sở, hoặc lớp cha.

Việc thực hiện thao tác thừa kế là đơn giản và hiệu quả, trong đó nó cho phép tạo một lớp mới từ một lớp có sẵn mà đã có mã lệnh và dữ liệu yêu cầu. Lớp mới dẫn xuất từ lớp có sẵn có thể tái sử dụng các biến và phương thức của nó mà không cần phải viết lại hoặc sửa lại đoạn mã.

Lớp con thừa kế tất cả các thành phần như là biến, lớp lồng, và những phương thức từ lớp cha của nó ngoại trừ những thành phần private. Tuy nhiên, các hàm tạo của một lớp không được coi là thành phần của nó và vì vậy chúng không được thừa kế bởi các lớp con. Tuy vậy, từ lớp con có thể gọi hàm tạo của lớp cha từ chính hàm tạo của nó.

Các thàn phần có mức truy cập mặc định trong lớp cha không được thừa kế bởi lớp con của những gói khác. Những thành phần này chỉ có thể được truy cập bởi các lớp con trong cùng một gói với lớp cha. Lớp con sẽ có những đặc điểm đặc trưng của chính nó cùng với những gì được thừa kế từ lớp cha. Có một số kiểu thừa kế được thể hiện ở hình dưới đây.

Các kiểu thừa kế trong Java

Dưới đây là những giải thích cụ thể cho các kiểu thừa kế khác nhau trong Java:

- Đơn thừa kế: Xảy ra khi một lớp con thừa kế từ chỉ một lớp cha. Ở hình trên, lớp B thừa kế từ một lớp đơn A là một ví dụ về đơn thừa kế.

- Thừa kế nhiều mức: Khi một lớp con dẫn xuất từ một lớp cha và bản thân lớp cha lại là con của một lớp khác thì được gọi là thừa kế nhiều mức. Lớp C ở hình trên thừa kế từ lớp B và lớp B lại thừa kế từ lớp A là thể hiện của kiểu thừa kế này.

- Thừa kế phân cấp: Loại thừa kế này xảy ra khi một lớp cha có nhiều hơn một lớp con ở những mức khác nhau. Ở hình trên, thể hiện của kiểu thừa kế này là lớp C thừa kế từ lớp B và lớp B cùng với lớp D thừa kế từ lớp A.

- Đa thừa kế: Xảy ra khi một lớp con dẫn xuất từ nhiều hơn một lớp cha. Java không hỗ trợ đa thừa kế. Điều này có nghĩa là, một lớp trong Java không thể thừa kế từ nhiều hơn một lớp cha. Tuy nhiên, Java cung cấp một cách giải quyết khác là cho phép một lớp thừa kế từ nhiều Interface (giao diện).

Làm việc với lớp cha và lớp con

Trong một lớp con, ta có quyền sử dụng các thành phần được thừa kế, ví dụ như ẩn chúng, thay thế chúng, hoặc tăng cường chúng. Ngoài ra, ta cũng có thể thực hiện những điều sau ở lớp con:

- Các thành phần được thừa kế, bao gồm các trường và phương thức, có thể được sử dụng trực tiếp như những trường khác.

- Ta có thể khai báo một trường với tên giống với tên trường của lớp cha. Điều này sẽ dẫn đến ẩn đi trường của lớp cha và như vậy việc này không được khuyến khích.

- Ta có thể khai báo những trường mới mà không có trong lớp cha. Những thành phần này sẽ được xác định ở lớp con.

- Ta có thể viết một phương thức thể hiện mới có chữ ký tương tự phương thức trong lớp cha. Điều này gọi là ghi đè phương thức.

- Một phương thức tĩnh mới có thể được tạo trong lớp con với chữ ký tương tự phương thức trong lớp cha. Điều này dẫn đến ẩn đi phương thức của lớp cha.

- Ta có thể khai báo những phương thức mới trong lớp con mà không có trong lớp cha.

- Một hàm tạo của lớp con có thể được sử dụng để gọi hàm tạo của lớp cha, có thể gọi ngầm định hoặc sử dụng từ khóa super.

Từ khóa extends được dùng khi tạo lớp con thừa kế từ lớp cha. Nếu một lớp không có bất kỳ lớp cha nào, thì ngầm định nó được dẫn xuất từ lớp Object. Cú pháp để tạo một lớp con thừa kế là như sau:

class <tên_lớp_con> extends <tên_lớp_cha> {


}

Đoạn mã 1 sau trình bày cách tạo lớp cha có tên PhuongTien.

Đoạn mã 1:

public class PhuongTien {
  // Khai báo các thuộc tính chung của một phương tiện giao thông
  protected String maPhuongTien; // Biến lưu trữ mã xe
  protected String tenPhuongTien; // Biến lưu tên xe
  protected int soBanh; // Lưu số lượng bánh xe
  public void tangToc(int tocDo) {
 /* Phương thức tăng tốc xe */
    System.out.println("Đang tăng tốc ở: " + tocDo + " km/h");
  }
}

Đoạn mã 2 dưới đây trình bày cách tạo lớp con có tên BonBanh.

Đoạn mã 2:

class BonBanh extends PhuongTien {
  // Khai báo một trường của lớp con
  private boolean troLuc; // Biến lưu trữ thông tin về trợ lực
  /* Hàm tạo có tham số để khởi tạo các giá trị dựa trên số liệu nhập liệu từ người dùng */
  public BonBanh(String maPhuongTien, String tenPhuongTien, int soBanh, boolean troLuc) {
    // Các thuộc tính được thừa kế từ lớp cha
    this.maPhuongTien = maPhuongTien;
    this.tenPhuongTien = tenPhuongTien;
    this.soBanh = soBanh;
    // Thuộc tính của lớp con
    this.troLuc = troLuc;
  }
  public void hienThi() { /* Hiển thị thông tin chi tiết của xe */
    System.out.println("Mã xe:" + maPhuongTien);
    System.out.println("Tên xe:" + tenPhuongTien);
    System.out.println("Số bánh:" + soBanh);
    if(troLuc == true) {
          System.out.println("Trợ lực tay lái: Có");
      }
    else {
          System.out.println("Trợ lực tay lái: Không");
      }
  }
}
/* Định nghĩa lớp TestPhuongTien */
class TestPhuongTien {
  public static void main(String[] args) {
    // Tạo một đối tượng của lớp con
    BonBanh objBB = new BonBanh("CA-30 TO-2016", "Camry", 4, true);
    objBB.hienThi(); // Gọi phương thức của lớp con
    objBB.tangToc(200); // Gọi phương thức được thừa kế
  }
}

Đoạn mã trên mô tả lớp con BonBanh với thuộc tính riêng của nó là troLuc. Các giá trị cho các thuộc tính được thừa kế và thuộc tính riêng của nó được xác định trong hàm tạo. Phương thức hienThi() được dùng để hiển thị tất cả các chi tiết của lớp con.

Trong phương thức main() của lớp TestPhuongTien, đối tượng objBB của lớp con được tạo và hàm tạo có tham số được gọi bằng cách truyền các đối số khác nhau. Tiếp đó, phương thức hienThi() được gọi để in ra thông tin chi tiết. Ngoài ra, đối tượng của lớp con được sử dụng để gọi phương thức tangToc() được thừa kế từ lớp cha và giá trị về tốc độ được truyền như là đối số.

Từ đoạn mã 2 ta suy ra rằng ta có thể truy cập các thành phần protected và public của lớp cha một cách trực tiếp trong lớp con.

Xem thêm

Đa hình (Polymorphism)

Giới thiệu

Trong thực tế, những động vật như tắc kè hoa chẳng hạn có khả năng thay đổi màu sắc dựa trên môi trường. Một người nào đó có thể đóng các vai trò khác nhau trong cuộc sống hàng ngày của anh ta, như là cha, con, chồng. Như vậy thì trong những tình huống khác nhau thì anh ta cũng hành xử khác nhau. Tương tự như vậy, Java cung cấp đặc điểm gọi là đa hình (polymorphism) trong đó một đối tượng có thể có những hành xử khác nhau dựa trên bối cảnh mà nó được sử dụng.

Từ 'polymorph' là sự kết hợp của hai từ, 'poly' nghĩa là 'many' và 'morph' nghĩa là 'forms'. Vì thế, 'polymorphism' là 'đa hình', nó tham chiếu tới một đối tượng mà có thể có nhiều dạng. Nguyên tắc này cũng có thể áp dụng cho các lớp con của một lớp mà có thể định nghĩa các hành vi đặc trưng của chính chúng cũng như dẫn xuất một số chức năng tương tự của lớp cha. Khái niệm ghi đè phương thức là một ví dụ về đa hình trong lập trình hướng đối tượng trong đó cùng một phương thức nhưng lại cư xử khác nhau giữa lớp cha và lớp con.

Hiểu rõ về liên kết tĩnh và liên kết động

Khi trình biên dịch giải quyết liên kết của các phương thức và các lời gọi phương thức lúc biên dịch, nó được gọi là liên kết tĩnh hay liên kết sớm. Nếu trình biên dịch giải quyết các lời gọi phương thức và các liên kết trong thời gian chạy (runtime), nó được gọi là liên kết động hoặc liên kết muộn. Tất cả các lời gọi phương thức tĩnh đều được giải quyết lúc biên dịch và do đó, liên kết tĩnh được thực hiện cho tất cả các lời gọi phương thức tĩnh. Các lời gọi phương thức thể hiện luôn luôn được giải quyết lúc thực thi chương trình.

Các phương thức tĩnh là các phương thức lớp và được truy cập bằng cách sử dụng tên của chính lớp đó. Việc sử dụng phương thức tĩnh là được khuyến khích, bởi vì các tham chiếu đối tượng không được yêu cầu để truy cập chúng và do đó, các lời gọi phương thức tĩnh được giải quyết trong quá trình biên dịch chính chúng. Đó cũng là lý do vì sao mà các phương thức tĩnh không được ghi đè.

Tương tự như vậy, Java không cho phép có hành vi đa hình trong các biến của một lớp bất kỳ. Do đó, việc truy cập đến tất cả các biến cũng là theo liên kết tĩnh.

Một số điểm khác biệt quan trọng giữa liên kết tĩnh và liên kết động được thể hiện như bảng dưới đây.

Liên kết tĩnh Liên kết động
Liên kết tĩnh xảy ra khi biên dịch. Liên kết động xảy ra khi thực thi.
Các phương thức và biến private, static, và final sử dụng liên kết tĩnh và được quy định bởi trình biên dịch. Các phương thức trừu tượng được quy định lúc thực thi và dựa trên đối tượng thực thi.
Liên kết tĩnh sử dụng thông tin kiểu đối tượng để liên kết, đó là kiểu của lớp. Liên kết động sử dụng kiểu tham chiếu để giải quyết liên kết.
Các phương thức được tải chồng được quy định sử dụng liên kết tĩnh. Các phương thức được ghi đè được quy định sử dụng liên kết động.

Đoạn mã 1 thể hiện một ví dụ về liên kết tĩnh.

Đoạn mã 1:

class NhanVien {
  String idNV; // Biến để lưu mã nhân viên
  String tenNV; // Lưu tên nhân viên
  int luongNV; // Lưu lương
  float hoaHong; // Lưu hoa hồng
  /* Hàm tạo có tham số để khởi tạo các biến */
  public NhanVien(String id, String ten, int luong) {
    idNV = id;
    tenNV = ten;
    luongNV = luong;
  }
  /* Tính hoa hồng dựa trên doanh số bán hàng */
  public void tinhHoaHong(float doanhSo) {
    if(doanhSo > 10000)
      hoaHong = luongNV * 20 / 100;
    else
      hoaHong = 0;
  }
  /* Tải chồng chương thức. Tính hoa hồng dựa trên thời gian vượt giờ */
  public void tinhHoaHong(int vuotGio) {
    if(vuotGio > 8)
      hoaHong = luongNV/30;
    else
      hoaHong = 0;
  }
  /* Hiển thị thông tin chi tiết nhân viên */
  public void chiTietNhanVien() {
    System.out.println("Mã nhân viên: " + idNV);
    System.out.println("Tên nhân viên: " + tenNV);
    System.out.println("Lương: " + luongNV);
    System.out.println("Hoa hồng: " + hoaHong);
  }
}
  /* Định nghĩa lớp TestNhanVien */
  class TestNhanVien {
  public static void main(String[] args) {
    // Tạo đối tượng của lớp NhanVien
    NhanVien objNV = new NhanVien("NV001", "Phan Ba Sang", 5000000);
    // Gọi phương thức tinhHoaHong() với đối số kiểu float
    objNV.tinhHoaHong(3000000F);
    // In ra thông tin chi tiết nhân viên
    objNV.chiTietNhanVien();
  }
}

Đoạn mã 1 cho thấy lớp NhanVien có bốn biến thành phần. Hàm tạo được sử dụng để khởi tạo các biến thành phần với các giá trị nhận được. Lớp bao gồm hai phương thức tinhHoaHong(). Phương thức thứ nhất tính hoa hồng dựa trên doanh số bán hàng được thực hiện bởi nhân viên và những tính toán khác dựa trên số giờ làm việc ngoài giờ của nhân viên. Phương thức chiTietNhanVien() được dùng để in ra chi tiết thông tin nhân viên.

Một lớp khác là lớp TestNhanVien được tạo chứa phương thức main(). Bên trong phương thức main(), đối tượng objNV của lớp NhanVien được tạo và hàm tạo có tham số được gọi với các đối số khác nhau. Tiếp theo, phương thức tinhHoaHong() được gọi với đối số là 3000000F. Khi phương thức tinhHoaHong() được thực thi thì phương thức tương ứng với đối số có kiểu float được gọi đến bởi vì nó được quy định trong quá trình biên dịch dựa trên kiểu của biến. Sau cùng, phương thức chiTietNhanVien() được gọi để in ra chi tiết thông tin của nhân viên.

Đoạn mã 2 thể hiện một ví dụ về liên kết động.

Đoạn mã 2:

class NhanVienPastTime extends NhanVien {
  // Biến của lớp con
  String chuyenCa; // Biến này dùng để lưu trữ thông tin chuyển đổi ca làm
  /* Hàm tạo có tham số để khởi tạo các giá trị dựa trên giá trị nhập vào từ người dùng */
  public NhanVienPastTime(String id, String ten, int luong, String chuyenCa) {
    // Gọi hàm tạo lớp cha
    super(id, ten, luong);
    this.chuyenCa = chuyenCa;
  }
  /* Ghi đè phương thức để hiển thị thông tin chi tiết của nhân viên */
  @Override public void chiTietNhanVien() {
    tinhHoaHong(12); // Gọi phương thức đã thừa kế
    super.chiTietNhanVien(); // Gọi phương thức hiển thị của lớp super
    System.out.println("Chuyển ca làm: "+shift);
  }
}
/* Thay đổi định nghĩa lớp TestNhanVien */
class TestNhanVien{
  public static void main(String[] args) {
    // Tạo đối tượng lớp NhanVien
    NhanVien objNV = new NhanVien("NV001", "Dinh Thi Kim Ngan", 4000000);
    objNV.tinhHoaHong(3000000F); // Tính hoa hồng
    objNV.chiTietNhanVien(); // In thông tin chi tiết của objNV
    System.out.println("-------------------------");
    /* Tạo biến tham chiếu của lớp NhanVien nhưng lại tham chiếu đến đối tượng của lớp NhanVienPartTime */
    NhanVien objNV1 = new NhanVienPartTime("NV002", "Bui Quynh Hoa", 3000000, "Ca sang");
    objNV1.chiTietNhanVien(); // In thông tin chi tiết của objNV1
  }
}

Đoạn mã 2 hiển thị lớp NhanVienPartTime thừa kế từ lớp NhanVien đã tạo ở đoạn mã 1. Lớp này có biến riêng của nó là chuyenCa để chỉ ra rằng nhân viên làm ca ngày hay ca đêm. Hàm tạo của lớp NhanVienPartTime gọi hàm tạo của lớp cha sử dụng từ khóa super để khởi tạo các thuộc tính chung của nhân viên. Ngoài ra, nó cũng khởi tạo cho biến chuyenCa.

Lớp con ghi đè phương thức chiTietNhanVien(). Bên trong phương thức được ghi đè, phương thức tinhHoaHong() được gọi đến với một đối số kiểu nguyên. Nó sẽ tính hoa hồng dựa trên giờ làm thêm. Tiếp theo, phương thức chiTietNhanVien() của lớp cha được gọi để hiển thị nhưng thông tin cơ bản của nhân viên cũng như chi tiết về chuyenCa.

Lớp TestNhanVien được sửa đổi trong đó tạo một đối tượng khác là objNV1 của lớp NhanVien. Tuy nhiên, đối tượng được gán tham chiếu của lớp NhanVienPartTime và hàm tạo được gọi là hàm tao bốn đối số. Sau đó, phương thức chiTietNhanVien() được gọi để in ra chi tiết nhân viên.

Chú ý rằng đầu ra của nhân viên mã "NV002" cũng thể hiện chi tiết của biến chuyenCa. Điều này chỉ ra rằng phương thức displayDetails() của lớp con NhanVienPartTime được gọi mặc dù kiểu của đối  tượng objNV1 là NhanVien. Đó là bởi vì, trong quá trình tạo nó lưu trữ tham chiếu của lớp NhanVienPartTime.

Đây là liên kết động, đó là lời gọi phương thức được quy định cho đối tượng lúc thực thi dựa trên tham chiếu được gán cho đối tượng.

Sự khác nhau giữa kiểu tham chiếu và kiểu đối tượng

Trong đoạn mã 2, kiểu của của đối tượng objNv1 là NhanVien. Điều này có nghĩa là đối tượng sẽ có tất cả các đặc điểm của lớp NhanVien. Tuy nhiên, tham chiếu được gán cho đối tượng của lớp NhanVienPartTime. Điều này có nghĩa là đối tượng sẽ liên kết với các thành phần của lớp NhanVienPartTime trong quá trình chạy. Trong trường hợp này, kiểu đối tượng là NhanVien và kiểu tham chiếu là NhanVienPartTime. Điều này chỉ có thể xảy ra khi các lớp có mối liên quan theo quan hệ cha-con.

Java cho phép gán một thể hiện của lớp con cho lớp cha của nó. Điều này gọi là upcasting.

Ví dụ,

NhanVienPartTime objNVPT = new NhanVienPartTime();
NhanVien objNV = objNVPT; // upcasting

Trong khi upcasting một đối tượng, thì đối tượng con objNVPT được gán trực tiếp cho đối tượng objNV của lớp cha. Tuy nhiên, đối tượng cha không thể truy cập các thành phần riêng của đối tượng con và không có sẵn trong lớp cha.

Java cũng cho phép ép kiểu tham chiếu cha trở về kiểu con. Điều này là bởi cha tham chiếu một đối tượng có kiểu con. Ép kiểu một đối tượng cha sang kiểu con được gọi là downcasting bởi vì một đối tượng có quyền được ép kiểu sang một lớp nhỏ hơn trong cấu trúc phân cấp. Tuy nhiên, downcasting yêu cầu ép kiểu tường minh bằng cách xác định tên lớp con trong cặp ngoặc tròn. Ví dụ,

NhanVienPartTime objNVPT = (NhanVienPartTime) objNV; // downcasting

Lời gọi phương thức ảo

Trong đoạn mã 2, trong quá trình thực thi câu lệnh NhanVien objNV1= new NhanVienPartTime(…);, kiểu runtime (thời gian thực thi) của đối tượng NhanVien được xác định. Trình biên dịch không phát sinh lỗi bởi vì lớp NhanVien cũng có phương thức chiTietNhanVien(). Tại thời điểm chạy, phương thức đã thực thi được tham chiếu từ đối tượng lớp NhanVienPartTime. Khía cạnh này của đa hình gọi là lời gọi phương thức ảo.

Sự khác nhau ở đây là giữa trình biên dịch và thời gian thực thi. Trình biên dịch kiểm tra khả năng truy cập của mỗi phương thức và biến thể hiện dựa trên định nghĩa lớp, trong khi đó hành vi liên quan đến một đối tượng được xác định tại thời gian thực thi.

Đây là một khía cạnh quan trọng của đa hình trong đó hành vi của đối tượng được xác định tại thời điểm thực thi dựa trên tham chiếu được truyền tới nó.

Ở đây, vì đối tượng được tạo là thuộc về lớp NhanVienPartTime, nên phương thức chiTietNhanVien() của NhanVienPartTime được gọi mặc dù đối tượng có kiểu NhanVien. Điều này được tham chiếu như là lời gọi phương thức ảo và phương thức được tham chiếu tới như là phương thức ảo.

Trong Java, tất cả các phương thức đều hành xử theo cách này, theo đó một phương thức được ghi đè trong lớp con được gọi tại thời điểm thực thi không phân biệt kiểu tham chiếu được sử dụng trong mã nguồn lúc biên dịch. Trong các ngôn ngữ khác như C++, ta cũng có thể đạt được điều tương tự bằng cách sử dụng từ khóa virtual.

Đóng gói (Encapsulation)

Tổng quan

Xét một tình huống trong đó bạn muốn học cách lái xe ôtô. Như một người học, bạn không cần phải hiểu xem ôtô làm việc thế nào, cấu tạo bình xăng thế nào, nguyên lý của việc rẽ phải rẽ trái ra làm sao, ... Như một người lái xe, bạn quan tâm đến việc khởi động và tắt xe cũng như cách cung cấp nhiên liệu cho xe. Ngoài ra, bạn có thể muốn tìm hiểu cách sử dụng cần số và hãm phanh để hiểu một cách đầy đủ về chiếc xe. Vì thế, để lái một ôtô, bạn chỉ cần quan tâm đến giao diện (interface) của nó, mà không cần biết về cách thức hoạt động của các thành phần hay bộ phận của xe.

Tương tự như vậy, trong lập trình hướng đối tượng, khái niệm che giấu đi những chi tiết của một đối tượng được thực hiện bằng cách sử dụng biện pháp Đóng gói (Encapsulation).

Đóng gói là một cơ chế liên kết mã lệnh và dữ liệu với nhau trong một lớp. Tất cả các chi tiết thực thi của một lớp không cần phải hiện diện so với các lớp khác cũng như các đối tượng mà sử dụng nó. Thay vào đó, chỉ những thông tin cần thiết thì mới hiện diện so với các thành phần khác của ứng dụng và phần còn lại có thể được ẩn đi. Điều này được thực hiện thông qua cơ chế đóng gói. Nói cách khác, có thể nói rằng trong các ngôn ngữ lập trình hướng đối tượng, đóng gói bao gồm các hoạt động bên trong của một đối tượng Java. Mục đích chính của việc che dấu trong một lớp là để giảm đi sự phức tạp trong phát triển phần mềm. Bằng cách che dấu những chi tiết thực thi về những gì được yêu cầu để thực hiện những hoạt động cần thiết trong lớp, việc sử dụng các hoạt động trở nên đơn giản hơn. Trong Java, việc che dấu được thực hiện bởi các bổ từ truy cập.

Đóng gói dữ liệu che dấu những biến thể hiện mà thể hiện trạng thái của đối tượng. Vì vậy, việc tương tác hay thay đổi dữ liệu đối với các loại biến thể hiện này được thực hiện thông qua các phương thức. Ví dụ, để thay đổi tên của chủ tài khoản, một phương thức riêng có tên chẳng hạn như setTenChuTaiKhoan() có thể được cung cấp trong lớp TaiKhoan, hoặc muốn xem thông tin về tên của chủ tài khoản ta có thể xây dựng phương thức getTenChuTaiKhoan(). Những phương thức như vậy thường được gọi là các setter và getter.

Các bổ từ truy cập

Bổ từ truy cập xác định cách mà các thành phần của lớp có thể được truy cập từ bên ngoài lớp đó. Nói cách khác, bổ từ truy cập định nghĩa tầm vực hay mức độ che dấu của các thành phần. Các bổ từ truy cập thường được đặt ở vị trí đầu tiên trong khi khai báo các thành phần của lớp. Phạm vi hay tầm vực của các thành phần cũng có liên hệ mật thiết với các gói.

Java cung cấp bốn loại bổ từ truy cập như sau:

- public: Cung cấp cấp độ truy cập rộng nhất. Các thành phần khai báo là public có thể được truy cập từ bất kỳ đâu trong lớp cũng như từ tất cả các lớp khác.

- private: Cung cấp cấp độ truy cập hẹp nhất. Các thành phần private chỉ có khả năng được truy cập từ bên trong lớp mà chúng được khai báo.

- protected: Bổ từ truy cập này cho phép các thành phần lớp có thể được truy cập từ bên trong lớp chứa chúng cũng như từ các lớp dẫn xuất.

- package (default): Bổ từ truy cập này chỉ cho phép các thành phần public của một lớp có thể truy cập có thể truy cập tất cả các lớp có trong cùng một gói. Đây là mức truy cập mặc định cho tất cả các thành phần của lớp trong trường hợp ta không khai báo bổ từ truy cập.

Như là một quy tắc chung trong Java, những chi tiết cũng như sự thực thi của một lớp được che dấu đi từ các lớp khác hoặc những đối tượng mở rộng trong ứng dụng. Điều này được thực hiện bằng cách đặt các biến thể hiện có mức truy cập là private và các phương thức thể hiện có mức truy cập là public.

Đoạn mã 1 dưới đây cho thấy việc sử dụng khái niệm đóng gói trong lớp ChuNhat.

Đoạn mã 1:

public class ChuNhat {
  //khai báo các biến thể hiện
  private int rong;
  private int cao;
  /* Định nghĩa hàm tạo không đối số */
  public ChuNhat() {
    System.out.println("Hàm tạo được gọi ...");
    rong = 10;
    cao = 10;
  }
  /* Định nghĩa hàm tạo hai đối số */
  public ChuNhat(int rong, int cao) {
    System.out.println("Hàm tạo hai đối số được gọi ...");
    this.rong = rong;
    this.cao = cao;
  }
  /* Hiển thị các chiều của đối tượng hình chữ nhật */
  public void hienThiCacChieu(){
    System.out.println("Rộng: " + rong);
    System.out.println("Cao: " + cao);
  }
}

Đoạn mã 1 ở trên thay đổi bổ từ truy cập của các biến thể hiện là rong và cao của lớp ChuNhat từ default thành private. Điều này có nghĩa là không thể trực tiếp truy cập các trường này từ bên ngoài lớp. Mục đích của việc này là để hạn chế việc truy cập đến các thành phần dữ liệu của lớp. Tương tự, bổ từ truy cập cho các phương thức được thay đổi thành public. Do đó, người dùng có thể truy cập các thành phần lớp thông qua các phương thức của nó mà không ảnh hưởng đến việc thực thi bên trong của lớp.

Setter và Getter

Ở đoạn mã 1 bên trên, vì các biến thể hiện rongcao được đóng gói là private, nên ta không thể truy cập đến chúng từ bên ngoài lớp ChuNhat. Tuy nhiên, có một số tình huống mà ta cần phải lấy hoặc thay đổi giá trị cho những biến thể hiện này, ví dụ như sử dụng một phương thức tử một lớp khác để nhập liệu cho chúng. Để giải quyết vấn đề này, ta xây dựng hai loại phương thức là settergetter.

Phương thức setter

setter được xây dựng để gán hay thay đổi giá trị của biến thể hiện. Cú pháp xây dựng setter là như sau:

bổ_từ_truy_cập void tên_setter(tham_số) {
tên_biến_thể_hiện = tham_số;
}

trong đó,

bổ_từ_truy_cập: phải khác private.

tham_số: phải có kiểu tương đương với kiểu của tên_biến_thể_hiện.

Đoạn mã 2 dưới đây định nghĩa các phương thức setter cho hai biến rongcao của lớp ChuNhat.

Đoạn mã 2:

public void setRong(int rong) {
  this.rong = rong;
}
public void setCao(int cao) {
  this.cao = cao;
}

Phương thức getter

getter dùng để lấy giá trị của biến thể hiện. Cú pháp cho phương thức getter là như sau:

bổ_từ_truy_cập kiểu_trả_về tên_getter() {
  return tên_biến_thể_hiện;
}

trong đó,

bổ_từ_truy_cập: không được là private.

kiểu_trả_về: giống với kiểu của tên_biến_thể_hiện.

Đoạn mã 3 định nghĩa các phương thức getter cho hai biến thể hiện rong và cao.

Đoạn mã 3:

public void getRong() {
  return rong;
}
public void getCao() {
  return cao;
}

Xem thêm:

Trừu tượng (Abstract)

Trong khi thực hiện thừa kế, ta hoàn toàn có thể tạo đối tượng của lớp cha cũng như lớp con. Tuy nhiên, điều gì sẽ xảy ra nếu người dùng muốn hạn chế sử dụng trực tiếp lớp cha? Đó là, trong một số trường hợp, ta có thể muốn định nghĩa một lớp cha trong đó chỉ khai báo cấu trúc của thực thể đã cho mà không đưa ra sự thực thi nào từ mỗi phương thức. Như thế thì lớp cha phục vụ như là một mẫu khái quát và tất cả các lớp con sẽ thừa kế từ nó. Các phương thức của lớp cha phục vụ như là một hợp đồng hay một chuẩn mà trong đó lớp con có thể thực thi theo hướng riêng của nó. Java cung cấp từ khóa 'abstract' để thực hiện nhiệm vụ này.

Như vậy, một phương thức trừu tượng sẽ được khai báo với từ khóa 'abstract' và không có phần thực thi, tức là không có phần thân. Phương thức trừu tượng không chứa cặp ngoặc xoắn ({}) và kết thúc là dấu chấm phẩy (;). Cú pháp khai báo một phương thức trừu tượng là như sau:

abstract <kiểu_trả_về> <tên_phương_thức> (<danh_sách_tham_số>);

trong đó, từ khóa abstract chỉ ra rằng phương thức là một phương thức trừu tượng.

Ví dụ,

public abstract void tinhToan();

Một lớp trừu tượng sẽ chứa các phương thức trừu tượng. Lớp trừu tượng sẽ phục vụ như là một framework trong đó cung cấp các hành vi cho các lớp khác. Có một lưu ý là các lớp trừu tượng không thể có phần thể hiện (không tạo được đối tượng từ lớp trừu tượng thông qua toán tử new) và chúng phải được phân lớp (các lớp con) để sử dụng các thành phần lớp. Lớp con sẽ thực thi các phương thức trừu tượng trong lớp cha của nó. Cú pháp khai báo một lớp trừu tượng là như sau:

abstract class <tên_lớp> {
  // Khai báo các trường
  // Định nghĩa các phương thức cụ thể
  //[Khai báo các phương thức trừu tượng]
}

trong đó, abstract chỉ ra rằng lớp và phương thức là trừu tượng.

Ví dụ,

public abstract MayTinh {
  public float getPI() { // Định nghĩa một phương thức cụ thể
    return 3.14F;
  }
  abstract void tinhToan(); // Khai báo một phương thức trừu tượng
}

Đoạn mã 1 cho thấy việc tạo lớp và phương thức trừu tượng.

Đoạn mã 1:

abstract class HinhDang {
  private final float PI = 3.14F; // Biến để lưu giá trị của số PI
  /* getter của biến PI */
  public float getPI() {
    return PI;
  }
  /* Khai báo phương thức trừu tượng */
  abstract void tinhToan(float giaTri);
}

Trong đoạn mã trên, lớp HinhDang là một lớp trừu tượng với một phương thức cụ thể là getPI() và một phương thức trừu tượng là tinhToan().

Để sử dụng lớp trừu tượng, ta cần tạo các lớp con. Đoạn mã 2 dưới đây tạo hai lớp con là HinhTron và ChuNhat.

Đoạn mã 2:

/* Định nghĩa lớp con HinhTron */
class HinhTron extends HinhDang {
  float dienTich; // Biến để lưu diện tích của hình tròn
  /* Thực thi phương thức trừu tượng để tính diện tích hình tròn */
  @Override void tinhToan(float banKinh) {
    dienTich = getPI() * banKinh * banKinh;
    System.out.println("Diện tích của hình tròn là: " + dienTich);
  }
}
/* Định nghĩa lớp con ChuNhat */
class ChuNhat extends HinhDang {
  float chuVi; // Biến lưu chu vi
  float chieuDai = 10; // Biến lưu chiều dài
  /* Thực thi phương thức trừu trượng để tính chu vi */

  @Override void tinhToan(float chieuRong) {
    chuVi = 2 * (chieuDai + chieuRong);
    System.out.println("Chu vi của hình chữ nhật là: " + chuVi);
  }
}

Lớp HinhTron thực thi phương thức trừu tượng tinhToan() để tính diện tích hình tròn. Tương tự, lớp ChuNhat thực thi phương thức tinhToan() để tính chu vi của hình chữ nhật.

Đoạn mã 3 mô tả mã lệnh của lớp MayTinh trong đó sử dụng các lớp con dựa trên các giá trị nhập vào từ người dùng.

Đoạn mã 3:

public class MayTinh {
  static Scanner sc = new Scanner(System.in);
  public static void main(String[] args) {
    HinhDang objHinhDang; // Khai báo 1 đối tượng của lớp HinhDang
    String hinhDang; // Biến lưu loại hình dạng
    System.out.print("Mời nhập hình dạng bạn muốn thao tác (HinhTron/ChuNhat): ");
    hinhDang = sc.nextLine();
    hinhDang = hinhDang.toLowerCase(); // chuyển sang chữ thường
    System.out.print("Mời bạn nhập chiều rộng: ");
    float rong = sc.nextFloat();
    switch (hinhDang) { // Kiểm tra hình dạng (phiên bản JDK7 trở lên switch mới hỗ trợ kiểu chuỗi)
      case "hinhtron":
        objHinhDang = new HinhTron();
        objHinhDang.tinhToan(rong);
        break;
      case "chunhat":
        objHinhDang = new ChuNhat();
        objHinhDang.tinhToan(rong);
        break;
      default:
        System.out.println("Bạn chỉ nhập được HinhTron hoặc ChuNhat");
    }
  }
}

Trong đoạn mã trên, bạn hãy để ý tới đối tượng objHinhDang của lớp HinhDang, ta đã đề cập ở phía đầu bài viết rằng một lớp trừu tượng không thể có thể hiện. Do đó, ta không thể viết là HinhDang objHinhDang = new HinhDang();. Tuy nhiên, một lớp trừu tượng có thể được quyền chỉ định một tham chiếu tới các lớp con của nó. Vì thế mà câu lệnh HinhDang objHinhDang; là hoàn toàn hợp lệ.

Câu lệnh switch kiểm tra hình dạng thông qua giá trị chuỗi chứa trong biến hinhDang (trước đó đã được chuyển thành chữ in thường bằng câu lệnh hinhDang = hinhDang.toLowerCase();) để gán tham chiếu cho phù hợp. Ví dụ, nếu hinhDang là hình tròn thì nó sẽ gán new HinhTron() như là tham chiếu, rồi khi sử dụng đối tượng objHinhDang thì phương thức tinhToan() ứng với lớp con được tham chiếu được gọi.

Tương tự, ta có thể cho một vài lớp khác thừa kế từ lớp trừu tượng HinhDang và thực thi phương thức tinhToan() theo yêu cầu.

Bài tập phần Class

Bài tập 1:

Tạo class có tên SoHoc gồm có các thuộc tính và phương thức sau:

+ Thuộc tính private: number1, number2

+ Phương thức:

- Các hàm tạo không đối số, đầy đủ đối số

- Các phương thức get, set cho tất cả các thuộc tính

- inputInfo(): dùng để nhập 2 số number1, number2

- printInfo(): dùng để hiển thị number1, number2

- addition(): dùng để cộng number1, number2

- subtract(): trừ number1, number2

- multi(): dùng để nhân number1, number2

- division(): dùng để chia number1, number2.

Bài tập 2:

Viết class NhanVien gồm các thuộc tính:

+ Tên

+ Tuổi

+ Địa chỉ

+ Tiền lương (kiểu double)

+ Tổng số giờ làm (kiểu int)

Constructor không tham số. Constructor đầy đủ tham số. Các hàm get/set

Và các phương thức:

- void inputInfo() : Nhập các thông tin cho nhân viên từ bàn phím

- void printInfo() : in ra tất cả các thông tin của nhân viên

- double tinhThuong(): Tính toán và trả về số tiền thưởng của nhân viên theo công thức sau:

Nếu tổng số giờ làm của nhân viên >=200 thì thưởng = lương * 20%

Nếu tổng số giờ làm của nhân viên <200 và >=100 thì thưởng = lương * 10%

Nếu tổng số giờ làm của nhân viên <100 thì thưởng = 0

Bài tập 3:

Tạo lớp Student, lưu trữ các thông tin một sinh viên:

- Mã sinh viên: chứa 8 kí tự

- Điểm trung bình: từ 0.0 – 10.0

- Tuổi: Phải lớn hơn hoặc bằng 18

- Lớp: Phải bắt đầu bởi kí tự ‘A’ hoặc kí tự ‘C’

Constructor không tham số. Constructor đầy đủ tham số. Các hàm get/set

a. phương thức inputInfo(), nhập thông tin Student từ bàn phím

b. phương thức showInfo(), hiển thị tất cả thông tin Student

c. Viết phương thức xét xem Student có được học bổng không? Điểm trung bình trên 8.0 là được học bổng

Bài tập 4:

Học viện V1Study thực hiện trao học bổng cho các học viên xuất sắc và đáp ứng đủ các yêu cầu sau:

a. Là học viên đăng ký khóa học HDSE

b. Có điểm tổng kết >= 75%

c. Không vi phạm nội quy của trung tâm

d. Các kì thi chỉ thi lần đầu tiên

Các dữ liệu a b c d của 1 học viên được nhập từ bàn phím.

Viết chương trình xem học viên đó có được học bổng không.

Bài tập 5:

Tạo class SoNguyenTo gồm:

biến a lưu trữ 1 số nguyên tố.

Constructor không tham số. 

Constructor 1 tham số: public SoNguyenTo(int x){}. Nếu x là số nguyên tố thì lưu x vào biến a. Nếu không thì in ra màn hình: x không phải là số nguyên tố, không lưu trữ.

Hàm boolean isSoNguyenTo(int x){} kiểm tra số x có phải số nguyên tố không.

Hàm int timSoNguyenToTiepTheo(){} tìm và trả về số nguyên tố liền sau số nguyên tố a.

Hàm get/set biến a. Nếu tham số truyền vào hàm set là 1 số nguyên tố thì mới gán vào a. Nếu không thì hiển thị thông báo: không set.

Ở hàm Main. Khai báo 1 đối tượng thuộc class SoNguyenTo và test các hàm đã viết.

Bài tập 6:

Xây dựng lớp tam giác (Triangle) có các thành phần:

* Các thuộc tính: 3 cạnh a, b, c của tam giác.

* Các phương thức:

- Nhập độ dài 3 cạnh

- Xác định kiểu tam giác

- Tính chu vi tam giác

- Tính diện tích tam giác

Bài tập 7:

Xây dựng lớp hình chữ nhật (Rectangle) có các thành phần sau:

* Các thuộc tính: chiều dài, chiều rộng.

* Các phương thức:

- Nhập chiều dài, chiều rộng

- Tính diện tích

- Tính chu vi

Bài tập 8:

Xây dựng lớp Phân số (Fraction) có các thành phần sau:

* Các thuộc tính: Tử số, mẫu số.

* Hàm tạo không đối số và hai đối số để khởi tạo giá trị cho tử số và mẫu số.

* Các phương thức setter và getter.

* Các phương thức:

- Nhập phân số

- In Phân số

- Rút gọn phân số

- Nghịch đảo phân số

- add(), sub(), mul(), div() tương ứng để thực hiện cộng, trừ, nhân, chia hai phân số cho nhau.

Bài tập 9:

Xây dựng lớp số phức có các thành phần sau:

+ Các thuộc tính:

- Phần thực

- Phần ảo

+ Các hàm thành phần:

- Cộng hai số phức

- Trừ hai số phức

- Nhân hai số phức

- Chia hai số phức

Nhập vào 2 số phức và thực hiện các phép toán trên hai số phức đó.

Bài tập 10:

Xây dựng lớp Vectơ có các thành phần sau:

+ Các thuộc tính:

- Hoành độ đầu

- Tung độ đầu

- Hoành đô cuối

- Tung độ cuối

+ Các phương thức:

- Kiểm tra hai vectơ có bằng nhau không?

- Tính góc giữa 2 vectơ

- Tính module của 2 vectơ

- Kiểm tra hai vectơ có cùng phương không?

- Cộng hai vectơ

- Trư hai vectơ

- Nhân hai vectơ

Nhập vào 2 vectơ và thực hiện các phép toán trên hai vectơ đó.

Bài tập 11:

Xây dựng lớp đa thức (polylomial) và các phép toán trên đa thức. Thực hiện nhập vào 2 đa thức và tính tổng, tích của nó.

Bài tập 12:

Xây dựng lớp ma trận và các phép toán trên ma trận. Thực hiện nhập vào 2 ma trận và tính tổng, tích của nó.

Bài tập 13:

Xây dựng lớp đa giác, hình bình hành thừa kế từ đa giác, hình chữ nhật thừa kế từ hình bình hành và hình vuông thừa kế từ hình chữ nhật. Nhập vào các thuộc tính cần thiết của mỗi hình và tính chu vi, diện tích của hình đó.

Bài tập 14:

Xây dựng lớp điểm, lớp elip thừa kế từ lớp điểm, lớp đường tròn thừa kế từ lớp elip. Nhập vào các thuộc tính cần thiết của elip và tính diện tích.

Bài tập 15:

Xây dựng lớp tam giác, lớp tam giác vuông, tam giác cân thừa kế từ lớp tam giác. Lớp tam giác đều thừa kế từ lớp tam giác cân.

Bài tập 16:

Mô phỏng sự hoạt dộng của một chiếc đèn pin. Với hai nhóm đối tượng cơ bản là Đèn (FlashLamp) và Pin (Battery). Đối tượng pin mang trong mình thông tin về trạng thái năng lượng, đối tượng đèn sữ sử dụng một đối tượng pin để cung cấp năng lượng cho hoạt động chiếu sáng.

Mô tả chi tiết lớp các đối tượng pin và đèn như sau:

FlashLamp

- status: boolean

- battery: Battery

+ FlashLamp()

+ setBattery(Battery): void

+ setBatteryInfo(): int

+ turnOn(): void

+ turnOff(): void

Battery

- energy: int

+ Battery()

+ setEnergy(int): void

+ getEnergy(int): int

+ decreaseEnergy(): void

1. Xây dựng lớp Pin (Battery) với các thuộc tính và các phương thức đã cho như trong sơ đồ trên. Trong đó:

- Thuộc tính: energy có kiểu số nguyên thể hiện năng lượng của Pin.

- Hàm tạo:

Battery(): Khởi tạo mặc định giá trị năng lượng Pin (energy) là 10

- Phương thức:

+ void setEnergy(int energy): Thiết đặt lại giá trị mới cho pin (thực hiện việc sạc pin)

+ int getEnergy(): Trả về thông tin năng lượng pin đang có

+ void decreaseEnergy(): mỗi lần Pin được sử dụng, năng lượng của Pin sẽ giảm đi 2 đơn vị.

2. Xây dựng lớp FlashLamp với các thuộc tính và phương thức như trong sơ đồ trên. Trong đó:

- Thuộc tính:

+ boolean status: trạng thái của đèn, nếu status = true tức đèn được bật, ngược lại đèn tắt.

+ Battery battery: pin của đèn

- Hàm tạo:

FlashLamp(): khởi tạo trạng thái đèn tắt và chưa có pin.

- Phương thức:

+ void setBattery(Battery b): nạp pin cho đèn

+ int getBatteryInfo(): lấy về năng lượng Pin của đèn

+ void turnOn(): Chuyển trạng thái đèn là true. In ra thông tin đèn có sáng hay không (năng lượng > 0 là sáng)

+ void turnOff(): Chuyển trạng thái đèn là false. In ra thông tin: Đèn tắt.

3. Xây dựng lớp TestFlashLamp chứa phương thức main() với kịch bản như sau:

- Tạo một đối tượng Battery

- Tạo một đối tượng FlashLamp

- Lắp pin cho đèn

- Bật và tắt đèn pin 10 lần

- Hiển thị ra màn hình mức năng lượng còn lại trong pin.

Bài tập 17:

Câu hỏi 1:

Một lớp gọi là MyPoint, thể hiện một mô hình điểm hai chiều (2D) gồm hai tọa độ x và y, được thiết kế theo dạng sơ đồ lớp. Lớp MyPoint bao gồm:

- Hai biến thể hiện x (kiểu int) và y (int).

- Hàm tạo không đối số dùng để khởi tạo một điểm có tọa độ (0,0).

- Một hàm tạo dùng để khởi tạo một điểm với tọa độ đã cho theo người dùng.

- Các phương thức getter và setter cho các biến thể hiện x và y.

- Một phương thức setXY() để set giá trị cho cả x và y.

- Phương thức toString() trả về mộ mô tả chuỗi theo định dạng "(x, y)".

- Một phương thức gọi là distance(int x, int y) trả về khoảng cách từ điểm này tới điểm khác tại các tọa độ (x, y) đã cho.

- Một phương thức tải chồng distance(MyPoint point1) trả về khoảng cách từ điểm hiện thời tới điểm point1.

Yêu cầu dành cho bạn: Viết mã lệnh xây dựng lớp MyPoint ở trên. Rồi viết một lớp có tên TestMyPoint để kiểm thử chương trình của bạn.

Gợi ý:

// Tải chồng phương thức distance()
public double distance(int x, int y) {
int xDiff = this.x - x;
int yDiff = ...
return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
}
public double distance(MyPoint another) {
int xDiff = this.x - another.x;
...
}
// Chương trình kiểm thử:
MyPoint p1=new MyPoint(3,0);
MyPoint p2=new MyPoint(0,4);
...
// Kiểm thử việc tải chồng phương thứcdistance():
System.out.println(p1.distance(p2));
System.out.println(p1.distance(5,6));

Câu hỏi 2:

Một lớp gọi là MyCircle, nó miêu tả một hình tròn có tâm là (x,y) và một bán kính. Lớp MyCircle sử dụng một thể hiện của lớp MyPoint (đã tạo ở câu 1) làm tâm của nó. Lớp MyCircle bao gồm:

- Hai biến thể hiện private: tâm (một thể hiện của MyPoint) và bán kính (có kiểu int).

- Một hàm tạo để khởi tạo hình tròn với tâm có tọa độ (x,y) và bán kính được đưa ra từ người dùng.

- Một hàm tạo khác để khởi tạo một hình tròn với tâm là thể hiện của lớp MyPoint và bán kính tương ứng.

- Các phương thức getter và setter.

- Phương thức toString() trả về một chuỗi mô tả hình tròn hiện thời theo dạng "Hình tròn @ (x,y) bán kính=r".

- Phương thức getArea() trả về diện tích của hình tròn.

Bạn hãy xây dựng lớp MyCircle. Ngoài ra, bạn viết lớp TestMyCircle để kiểm thử chương trình của bạn.

Bài tập 18: Hệ thống quản lý sản phẩm

1. Tạo một lớp có tên Product bao gồm các thuộc tính và phương thức sau:

· String Name

· String Description

· double Price // 0 < Price <= 100

· int[ ] rate // lưu các đánh giá của người dùng cho sản phẩm, giá trị từ 1 - 5

· void viewInfo() // hiển thị tên, giá và mô tả về sản phẩm

2. Tạo lớp Shop gồm các thuộc tính và phương thức sau:

· ArrayList ProductList // lưu danh sách các sản phẩm của shop

· void  addProduct()  // yêu cầu người dùng nhập thông tin của sản phẩm rồi lưu vào ProductList

· void removeProduct() // yêu cầu người dùng nhập vào tên sản phẩm sau đó tìm và xóa sản phẩm có tên tương ứng trong ProductList

· void iterateProductList() // hiển thị các sản phẩm trong ProductList, gọi phương thức  viewInfo() của lớp Product, tính trung bình cộng đánh giá cho từng sản phẩm và hiển thị thông tin ra màn hình.

· void searchProduct() // yêu cầu người dùng nhập vào 2 số, sau đó tìm và hiển thị thông tin của những sản phẩm có giá nằm giữa hai số đó.

3. Tạo menu:

PRODUCT MANAGEMENT SYSTEM

  1. Add new product
  2. Remove product
  3. Iterate product list
  4. Search product
  5. Exit

và thực thi các phương thức tương ứng trong lớp Shop với mỗi mục chọn.

Câu hỏi thêm:

Tạo thêm một mục trong Menu ứng với phương thức gọi là sortProduct() đặt trong lớp Shop để sắp xếp các sản phẩm trong ProductList theo giá.

Interface (Giao diện)

Giới thiệu

Java không hỗ trợ đa thừa kế. Tuy nhiên, có một số trường hợp khi nó trở thành bắt buộc đối với một đối tượng để thừa kế các thuộc tính từ nhiều lớp để tránh sự dư thừa và phức tạp trong mã lệnh. Với mục đích đó, Java cung cấp một cách giải quyết là sử dụng giao diện hay giao tiếp (interface).

Một giao diện trong Java là một hợp đồng quy định các chuẩn được theo sau bởi các kiểu thực thi nó. Các lớp mà chấp nhận hợp đồng thì phải tuân thủ nó.

Giao diện và lớp có một số điểm tương đồng nhau như sau:

- Giao diện có thể chứa nhiều phương thức.

- Một giao diện được lưu thành tập tin với đuôi mở rộng là .java và tên của tập tin phải trùng với tên của giao diện.

- Bytecode của một giao diện cũng được lưu thành một tập tin có đuôi mở rộng là .class.

- Các giao diện được lưu trong các gói và tập tin bytecode được lưu trong cấu trúc thư mục trùng tên với tên của gói.

Tuy nhiên, một giao diện và một lớp cũng có những điểm khác nhau như sau:

- Một giao diện thì không thể có thể hiện.

- Giao diện không thể có hàm tạo.

- Tất cả các phương thức của giao diện đều ngầm định là trừu tượng (abstract).

- Các trường được khai báo trong một giao diện phải bao gồm cả static và final. Giao diện không thể có các trường thể hiện.

- Một giao diện không được thừa kế nhưng được thực thi bởi một hoặc nhiều lớp.

- Một giao diện có thể thừa kế từ nhiều giao diện khác.

Mục đích của giao diện

Các đối tượng trong Java tương tác với thế giới bên ngoài với sự trợ giúp của các phương thức để tiếp xúc chúng. Vì vậy, có thể nói rằng, các phương thức phục vụ như là những giao diện của đối tượng với thế giới bên ngoài.

Điều này tương tự như những nút phía trước một chiếc tivi. Những nút này đóng vai trò như là một giao diện giữa người dùng và vi mạch điện tử và hệ thống dây điện ở phía bên kia của vỏ nhựa. Khi ta nhấn nút "Power", thì tivi sẽ được bật hoặc tắt.

Trong Java, một giao diện là một tập các phương thức quan hệ mà không có phần thân. Những phương thức này tạo thành các hợp đồng mà các lớp thực hiện phải đồng ý. Khi một lớp thực thi một giao diện thì nó trở thành chính thức hơn về hành vi nó hứa hẹn sẽ cung cấp. Hợp đồng này được thực thi tại thời điểm xây dựng bởi trình biên dịch. Nếu một lớp thực thi một giao diện, thì tất cả các phương thức được khai báo bởi giao diện đó phải xuất hiện trong lớp thực thi để việc biên dịch được thành công.

Vậy nên, trong Java, một giao diện là một kiểu tham chiếu mà tương tự như một lớp. Tuy nhiên, nó chỉ có thể chưa những chữ ký phương thức, hằng, và các kiểu lồng nhau. Không được định nghĩa phương thức mà chỉ được khai báo. Ngoài ra, không giống với lớp, các giao diện không thể được thể hiện và phải được thực thi bởi các lớp hoặc được thừa kế bởi những giao diện khác để sử dụng chúng.

Có một vài tính huống trong kỹ thuật phần mềm khi nó trở nên cần thiết cho những nhóm các nhà phát triển khác nhau phải đồng ý với một 'hợp đồng' để xác định cách tương tác với phần mềm của họ. Tuy nhiên, mỗi nhóm phải có sự tự do để viết mã lệnh của họ theo cách mà họ mong muốn mà không cần biết cách các nhóm khác đang viết mã lệnh thế nào. Các giao diện của Java có thể được sử dụng để định nghĩa những hợp đồng như vậy.

Các giao diện không thuộc về bất kỳ một hệ thống phân cấp lớp nào, mặc dù chúng hoạt động chung với các lớp. Java không hỗ trợ đa thừa kế, do vậy các giao diện được cung như một sự thay thế cho điều này. Trong Java, một lớp chỉ có thể thừa kế từ một lớp khác nhưng có thể thực thi nhiều giao diện. Do đó, các đối tượng của một lớp có thể có nhiều kiểu, chẳng hạn như kiểu của bản thân lớp của chúng hoặc là các kiểu giao diện mà các lớp thực thi. Cú pháp khai báo một giao diện là như sau:

interface <tên_giao_diện> extends <tên_giao_diện1, … >
{
  // khai báo các hằng
  // khai báo các phương thức trừu tượng
}

Ví dụ,

interface IDemo1 extends IDemo{
  static final int someInteger;
  public void someMethod();
}

Trong Java, tên giao diện được viết theo dang chữ lạc đà (CamelCase), đó là ký tự đầu của mỗi từ được viết hoa. Ngoài ra, tên của giao diện một tả một hoạt động mà lớp có thể giải quyết. Ví dụ,

interface Enumerable

interface Comparable

Một số lập trình viên lại thích đặt tiền tố 'I' ở đầu tên của giao diện để phân biệt giao diện với lớp. Ví dụ,

interface IEnumerable

interface IComparable

Lưu ý là việc khai báo phương thức là không có bất kỳ cặp ngoặc xoắn ({}) nào và phải kết thúc là dấu chấm phẩy (;). Ngoài ra, thân của giao diện chỉ chứa những phương thức trừu tượng và không có phương thức cụ thể. Tuy nhiên, vì tất cả các phương thức trong giao diện ngầm định là trừu tượng, nên ta không sử dụng từ khóa 'abstract' khi khao báo các phương thức của giao diện.

Khi một lớp thực thi một giao diện, nó phải thực thi tất cả các phương thức. Nếu lớp không thực thi tất cả các phương thức thì nó phải được đánh dấu là trừu tượng. Ngoài ra, nếu lớp thực thi giao diện là lớp trừu tượng, thì một trong những lớp con của nó phải thực thi những phương thức chưa được thực thi. Và nếu bất kỳ một lớp con nào của lớp trừu tượng không thực thi tất cả các phương thức của giao diện, thì lớp con đó cũng phải được đánh dấu là trừu tượng.

Các thành phần dữ liệu của giao diện ngầm định là dạng static, final, và public.

Ta xét một hệ thống phân cấp của các xe mà trong đó IVehicle là một giao diện và khai báo các phương thức cho các lớp thực thi như là TwoWheeler, FourWheeler, ... có thể thực thi. Để tạo một giao diện mới trong IDE NetBeans, ta kích phải chuột lên tên của gói và chọn New → Java Interface như thể hiện ở hình 11.1.

Interface: tạo một giao diện mới

Một hộp thoại xuất hiện trong đó người dùng phải cung cấp tên cho giao diện, rồi nhấn nút OK. Điều này sẽ tạo một giao diện với tên cụ thể.

Đoạn mã 1 định nghĩa giao diện IVehicle.

Đoạn mã 1:

public interface IVehicle {
// khai báo và khởi tạo hằng
static final String STATEID=”LA-09”; // variable to store state ID
/**
* phương thức trừu tượng khởi động xe
*/

public void start();
/**
* phương thức trừu tượng tăng tốc xe
*/

public void accelerate(int speed);
/**
* phương thức trừu tượng hãm phanh
*/

public void brake();
/**
* phương thức trừu tượng dừng xe
*/

public void stop();
}

Đoạn mã 1 định nghĩa một giao diện IVehicle với một hằng chuỗi tĩnh là STATEID. Ngoài ra, nó khai báo các phương thức trừu tượng như là start(), accelerate(int), brake(), và stop(). Để sử dụng giao diện này, thì một lớp được yêu cầu để thực thi giao diện. Lớp thực thi giao diện phải thực thi tất cả các phương thức này.

Cú pháp để thực thi một giao diện là như sau:

class <tên_lớp> implements <tên_giao_diện> {
  // các thành phần lớp
  // ghi đè các phương thức trừu tượng của giao diện
}

Đoạn mã 2 định nghĩa lớp TwoWheeler thực thi giao diện IVehicle ở đoạn mã 1.

Đoạn mã 2:

class TwoWheeler implements IVehicle {
String ID; // biến lưu ID của xe
String type; // biên lưu kiểu xe
/**
* hàm tạo có tham số để khởi tạo các giá trị nhập vào từ người dùng
*/
public TwoWheeler(String ID, String type){
this.ID = ID;
this.type = type;
}
/**
* ghi đè các phương thức để khởi động xe
*/
@Override
public void start() {
System.out.println(“Starting the “+ type);
}
/**
* ghi đè phương thức để tăng tốc xe
*/
@Override
public void accelerate(int speed) {
System.out.println(“Accelerating at speed:”+speed+ “ kmph”);
}
/**
* ghi đè phương thức để hãm phanh
*/
@Override
public void brake() {
System.out.println(“Applying brakes”);
}
/**
* ghe đè phương thức để dừng xe
*/
@Override
public void stop() {
System.out.println(“Stopping the “+ type);
}
/**
* hiển thị thông số của xe
*/
public void displayDetails(){
System.out.println(“Vehicle No.: “+ STATEID+ “ “+ ID);
System.out.println(“Vehicle Type.: “+ type);
}
}
public class TestVehicle {
public static void main(String[] args){
// xác nhận số lượng đối số dòng lệnh
if(args.length==3) {
// tạo thể hiện cho lớp TwoWheeler
TwoWheeler objBike = new TwoWheeler(args[0], args[1]);
// gọi các phương thức
objBike.displayDetails();
objBike.start();
objBike.accelerate(Integer.parseInt(args[2]));
objBike.brake();
objBike.stop();
}
else {
System.out.println(“Usage: java TwoWheeler <ID> <Type> <Speed>”);
}
}
}

Đoạn mã 2 định nghĩa lớp TwoWheeler thực thi giao diện IVehicle. Lớp này bao gồm một số biến thể hiện và một hàm tạo để khởi tạo giá trị cho các biến. Lưu ý rằng lớp thực thi tất cả các phương thức của giao diện IVehicle. Phương thức displayDetails() được dùng để hiển thị chi tiết của phương tiện giao thông cụ thể.

Phương thức main() được định nghĩa trong một lớp khác có tên TestVehicle. Trong phương thức main(), số lượng các đối số dòng lệnh cụ thể được xác nhận và phù hợp với đối tượng của lớp TwoWheeler được tạo. Tiếp theo, đối tượng được sử dụng để gọi các phương thức khác nhau của lớp.

Hình sau đây cho thấy output của đoạn mã trên khi người dùng truyền đối số dòng lệnh là CS-2723 Bike 80.

Thực thi nhiều giao diện

Java không hỗ trợ đa thừa kế các lớp nhưng cho phép thực thi nhiều giao diện để mô phỏng đa thừa kế. Để thực thi nhiều giao diện, ta viết tên các giao diện phía sau từ khóa implements và phân cách nhau bằng dấu phay (,). Ví dụ,

public class Sample implements Interface1, Interaface2{
}

Đoạn mã 3 định nghĩa giao diện IManufacturer.

Đoạn mã 3:

package session11;
public interface IManufacturer {
/**
* phương thức trừu tượng để thêm thông tin liên hệ
*/
public void addContact(String detail);
/**
* phương thức trừu tượng để gọi nhà sản xuất
*/
public void callManufacturer(String phone);
/**
* phương thức để tiến hành thanh toán
*/
public void makePayment(float amount);
}

Giao diện IManufacturer khai báo các phương thức trừu tượng gồm addContact(String), callManufacturer(String), và makePayment(float) mà phải được định nghĩa bởi lớp thực thi.

Lớp đã sửa đổi, TwoWheeler thực thi cả giao diện IVehicle và IManufacturer được hiển thị trong đoạn mã 4.

Đoạn mã 4:

package session11;
class TwoWheeler implements IVehicle, IManufacturer {
String ID; // ID xe
String type; // kiểu xe
public TwoWheeler(String ID, String type){
this.ID = ID;
this.type = type;
}
@Override
public void start() {
System.out.println(“Starting the “+ type);
}
@Override
public void accelerate(int speed) {
System.out.println(“Accelerating at speed:”+speed+ “ kmph”);
}
@Override
public void brake() {
System.out.println(“Applying brakes...”);
}
@Override
public void stop() {
System.out.println(“Stopping the “+ type);
}
public void displayDetails()
{
System.out.println(“Vehicle No.: “+ STATEID+ “ “+ ID);
System.out.println(“Vehicle Type.: “+ type);
}
// thực thi các phương thức của giao diện
@Override
public void addContact(String detail) {
System.out.println(“Manufacturer: “+detail);
}
@Override
public void callManufacturer(String phone) {
System.out.println(“Calling Manufacturer @: “+phone);
}
@Override
public void makePayment(float amount) {
System.out.println(“Payable Amount: $”+amount);
}
}
public class TestVehicle {
public static void main(String[] args){
// xác nhận đối số dòng lệnh
if(args.length==6) {
// thể hiện của lớp
TwoWheeler objBike = new TwoWheeler(args[0], args[1]);
objBike.displayDetails();
objBike.start();
objBike.accelerate(Integer.parseInt(args[2]));
objBike.brake();
objBike.stop();
objBike.addContact(args[3]);
objBike.callManufacturer(args[4]);
objBike.makePayment(Float.parseFloat(args[5]));
}
else{
// hiển thị thông báo lỗi
System.out.println(“Usage: java TwoWheeler <ID> <Type> <Speed>
<Manufacturer> <Phone> <Amount>”);
}
}
}

Lớp TwoWheeler bây giờ thực thi cả hai giao diện là IVehicle và IManufacturer, nghĩa là, nó thực thi tất cả các phương thức của cả hai giao diện.

Hình sau đây thể hiện output của đoạn mã đã sửa đổi ở trên, người dùng truyền đối số dòng lệnh là CS-2737 Bike 80 BN-Bikes 808-283-2828 300.

Lưu ý rằng giao diện IManufacturer chỉ có thể được thực thi bởi những lớp khác như FourWheeler, Furniture, Jewelry, và vân vân, mà yêu cầu thông tin nhà sản xuất.

Tìm hiểu khái niệm trừu tượng

Trừu tượng là một yếu tố thiết yếu của lập trình hướng đối tượng. Trong Java, nó được định nghĩa là quá trình ẩn đi những chi tiết không cần thiết và chỉ bộc lộ những tính năng thiết yếu của đối tượng cho người dùng. Trừu tượng là một khái niệm được sử dụng bởi các lớp trong đó bao gồm các thuộc tính và phương thức để thực hiện các hoạt động trên các thuộc tính này.

Tính trừu tượng cũng có thể được sử dụng thông qua các thành phần. Ví dụ, một lớp Vehicle bao gồm động cơ, lốp xe, khóa điện, và có thể gồm những thành phần khác. Để xây dựng lớp Vehicle, ta không cần phải biết về hoạt động bên trong của những thành phần khác nhau, mà chỉ cần biết làm thế nào để tương tác hoặt giao tiếp với chúng. Đó là, gửi và nhận tin nhắn đến và từ chúng cũng như làm cho các đối tượng khác nhau của lớp Vehicle tương tác với nhau.

Trong Java, các lớp trừu tượng và giao diện được dùng để thực thi khái niệm trừu tượng. Một lớp trừu tượng hoặc giao diện là không cụ thể, nói cách khác là chúng không hoàn chỉnh. Để sử dụng một lớp trừu tượng hoặc giao diện thì cần phải mở rộng hoặc thực thi các phương thức trừu tượng với các hành vi cụ thể theo bối cảnh trong đó nó được sử dụng. Tính trừu tượng được sử dụng để định nghĩa một đối tượng dựa trên các thuộc tính, chức năng, và giao diện của nó.

Sự khác nhau giữa lớp trừu tượng và giao diện được thể hiện ở bảng dưới đây.

Lớp trừu tượng Giao diện
Một lớp trừu tượng có thể có cả các phương thức trừu tượng và phương thức cụ thể là những phương thức có phần thân. Một giao diện chỉ có thể có các phương thức trừu tượng.
Một lớp trừu tượng có thể có những biến không final. Các biến được khai báo trong giao diện ngầm định là final.
Một lớp trừu tượng có thể có những thành phần với các đặc tả truy cập khác nhau như private, protected, public. Các thành phần của giao diện mặc định là public.
Lớp trừu tượng được thừa kế sử dụng từ khóa extends. Giao diện được thực thi sử dụng từ khóa implements.
Lớp trừu tượng có thể thừa kế từ một lớp và đồng thời thực thi từ nhiều giao diện. Một giao diện có thể mở rộng một hoặc nhiều giao diện khác.

Trừu tượng và Đóng gói là hai khái niệm quan trọng và thiết yếu trong lập trình hướng đối tượng Java. Hai khái niệm này hoàn toàn khác nhau. Trừu tượng dùng để đưa ra hành vi từ 'Làm thế nào để chính xác' với sự thực thi, trong khi đó Đóng gói dùng để ẩn đi những chi tiết thực thi từ thế giới bên ngoài để đảm bảo rằng bất kỳ sự thay đổi nào tới lớp đều không ảnh hưởng đến những lớp phụ thuộc. Một số điểm khác nhau của hai khái niệm này là như sau:

- Trừu tượng được thực thi sử dụng một giao diện và một lớp trừu tượng, trong khi đó Đóng gói được thực thi bằng cách sử dụng các bổ từ truy cập gồm private, default hoặc package-private, và protected.

- Đóng gói cũng còn được gọi là che dấu dữ liệu.

-  Cơ sở của nguyên tắc thiết kế 'programming for interface than implementation' là trừu tượng và 'encapsulate whatever changes' là đóng gói.

Ghi đè phương thức (Override)

Java cho phép lớp con có quyền tạo một phương thức giống hệt với phương thức của lớp cha. Điều này gọi là ghi đè phương thức (Override). Việc ghi đè này cho phép một lớp thừa kế hành vi từ một lớp khác nhưng có thể thay đổi hành vi đó khi cần.

Một số quy tắc cần nhớ khi ghi đè:

- Phương thức ghi đè phải cùng tên, kiểu, và số lượng đối số cũng như kiểu trả về với phương thức của lớp cha.

- Một phương thức ghi đè không thể có mức truy cập yếu hơn so với mức truy cập của phương thức tương ứng ở lớp cha.

Đoạn mã 1 sau đây định nghĩa lớp Vehicle.

Đoạn mã 1:

class Vehicle {
    protected String vehicleNo; // Variable to store vehicle number    
    protected String vehicleName; // Variable to store vehicle name
    protected int wheels; // Variable to store number of wheels
    //phương thức tăng tốc
    public void accelerate(int speed) {
        System.out.println("Accelerating at: " + speed + " kmph");
    }
}

Phương thức accelerate() trong đoạn mã 1 có thể được ghi đè như được thể hiện ở đoạn mã 2 dưới đây.

Đoạn mã 2:

class FourWheeler extends Vehicle {
  //Khai báo trường riêng của lớp con
  private boolean powerSteer; //Biến lưu trữ thông tin trợ lực
  public FourWheeler(String vId, String vName, int numWheels, boolean pSteer) {
    //các thuộc tính được thừa kế từ lớp cha
    vehicleNo=vId;
    vehicleName=vName;
    wheels=numWheels;
    //thuộc tính của riêng lớp con
    powerSteer=pSteer;
  }
  /**
  * Hiển thị thông tin chi tiết của xe
  */

  public void showDetails() {
    System.out.println("Mã xe: " + vehicleNo);
    System.out.println("Tên xe: " + vehicleName);
    System.out.println("Số bánh: " + wheels);
    if(powerSteer==true){
      System.out.println("Trợ lực: Có");
    else
      System.out.println("Trợ lực: Không");
  }
  /**
  * Ghi đè phương thức accelerate() của lớp cha
  */

  @Override public void accelerate(int speed) {
    System.out.println("Tăng tốc tối đa: " + speed + " km/h");
  }
}
class TestVehicle {
  public static void main(String[] args) {    
    FourWheeler objFour = new FourWheeler(“LA-09 CS-1406”, “Volkswagen”, 4, true);
    objFour.showDetails();
    objFour.accelerate(200); // Invoke inherited method
  }
}

Phương thức accelerate() được ghi đè trong lớp con với chữ ký và kiểu trả về tương tự nhưng với một lưu ý là cần đặt @Override ngay phía đầu phương thức. Đây là một chú thích nhằm hướng dẫn trình biên dịch rằng phương thức phía sau được ghi đè từ lớp cha. Nếu trình biên dịch nhận thấy rằng không có phương thức nào như vậy trong lớp cha thì nó sẽ tạo một lỗi biên dịch.

Lưu ý: Các chú thích sẽ cung cấp thêm thông tin về chương trình. Các chú thích không ảnh hưởng đến chức năng của đoạn mã mà chúng chú thích.

Output của đoạn mã trên được thể hiện như hình dưới đây:

demo-ghi-de-phuong-thuc-override

Lưu ý rằng phương thức accelerate() bây giờ sẽ in ra thông tin cụ thể trong lớp con. Điều này có nghĩa rằng một lời gọi tới phương thức accelerate() sử dụng đối tượng lớp con là objFour.accelerate() trước tiên sẽ tìm kiếm phương thức trong lớp con. Sau đó phương thức accelerate() được ghi đè trong lớp con, nó gọi phương thức accelerate() của lớp con mà không phải phương thức accelerate() của lớp cha.

switch-case

Cũng giống như if - else, điều kiện switch - case dùng để thiết lập các điều kiện rẽ nhánh, tuy nhiên, phạm vi áp dụng lại bó hẹp hơn, nó chỉ áp dụng được cho các hằng, ngoại trừ hằng số thực (float và double). Dưới đây là cú pháp cơ bản:

switch(Giá_trị) {
  case Hằng_1:
    Khối_lệnh_1;
    break;
  case Hằng_2:
    Khối_lệnh_2;
    break;
  ...
  case Hằng_n:
    Khối_lệnh_n;
    break;
  default:
    Khối_lệnh;
}

, trong đó: Giá_trị ở đây có thể là giá trị của biến, giá trị của biểu thức hoặc giá trị trả về từ một hàm. Nếu Giá_trị bằng với hằng của case nào thì Khối_lệnh tương ứng với case đó sẽ được thực hiện, thực hiện xong khối lệnh thì sẽ thoát khỏi switch...case thông qua câu lệnh break; ngay dưới khối lệnh tương ứng đó. Nếu Giá_trị không bằng bất kỳ hằng nào thì khối lệnh dưới default sẽ được thực hiện.

Lưu ý rằng default là không bắt buộc, nghĩa là bạn có thể bỏ đi phần default nếu bạn muốn cho phù hợp với bài toán của bạn.

Trong trường hợp có sự giống nhau của các Khối_lệnh ở các hằng khác nhau thì ta có cú pháp ngắn gọn hơn như sau:

switch(Giá_trị) {
  case Hằng_1:
  case Hằng_2:
  ...
  case Hằng_i:
    Khối_lệnh;
    break;
    ...
  default:
    Khối_lệnh0;
}

Ví dụ, hãy nhập vào một ký tự bất kỳ và xác định xem ký tự đó là nguyên âm hay phụ âm. Vì ngôn ngữ lập trình C không hỗ trợ ký tự có dấu nên ta chỉ xét các ký tự trong bảng chữ cái tiếng Anh. Những ký tự 'A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o' và 'u' là những nguyên âm. Những ký tự còn lại trong bảng chữ cái tiếng Anh là phụ âm. Vậy, ta có lời giải như sau:

package nguyenamphuam;

import java.io.IOException;
import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class NguyenAmPhuAm {

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        char ch;

        System.out.print("Nhập vào một ký tự (A-Z hoặc a-z): ");
        ch = (char) System.in.read();
        if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
            switch (ch) {
                case 'a':
                case 'A':
                case 'e':
                case 'E':
                case 'i':
                case 'I':
                case 'o':
                case 'O':
                case 'u':
                case 'U':
                    System.out.printf("'%c' là nguyên âm!", ch);
                    break;
                default:
                    System.out.printf("'%c' là phụ âm!", ch);
            }
        } else {
            System.out.printf("Ký tự nhập vào '%c' không phải nguyên âm hay phụ âm!", ch);
        }
    }

}

Ta xét thêm một ví dụ nữa, ví dụ này yêu cầu nhập vào một tháng bất kỳ và xác định xem tháng đó có bao nhiêu ngày.Dưới đây là những phân tích cho ví dụ này:

- Tháng ở đây là tháng theo lịch dương (lịch Tây).

- Những tháng sau có 31 ngày: 1, 3, 5, 7, 8, 10 và tháng 12.

- Những tháng sau có 30 ngày: 4, 6, 9 và tháng 11.

- Riêng tháng 2: nếu là năm nhuận thì có 29 ngày, năm thường thì có 28 ngày.

- Năm nhuận là năm thoả mãn một trong hai điều kiện sau:

+ Điều kiện 1: Năm đó chia hết cho 4 nhưng không chia hết cho 100.

+ Điều kiện 2: Năm đó chia hết cho 400.

Chẳng hạn, năm 2000 là năm nhuận vì mặc dù nó không thoả mãn điều kiện 1 nhưng nó thoả mãn điều kiện 2 là chia hết cho 400; năm 2016 là năm nhuận vì 2016 chia hết cho 4 nhưng không chia hết cho 100.

Sau đây là chương trình thực hiện yêu cầu trên:

package thangtrongnam;

import java.io.IOException;
import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class ThangTrongNam {

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        int thang, nam;

        do {
            System.out.print("Nhập vào một tháng bất kỳ: ");
            thang = sc.nextInt();
        } while (!(thang >= 1 && thang <= 12)); //trong khi tháng còn không nằm trong đoạn [1,12] thì còn yêu cầu nhập lại.
        switch (thang) {
            case 4:
            case 6:
            case 9:
            case 11:
                System.out.printf("Tháng %d có 30 ngày", thang);
                break;
            case 2:
                System.out.print("Nhập vào một năm: ");
                nam = sc.nextInt();
                if ((nam % 4 == 0 && nam % 100 != 0) || (nam % 400 == 0)) //nếu năm thỏa mãn một trong hai điều kiện đã phân tích ở trên
                {
                    System.out.printf("Tháng %d năm %d có 29 ngày", thang, nam); //thì tháng tương ứng có 29 ngày (năm nhuận)
                } else //nếu không thì
                {
                    System.out.printf("Tháng %d năm %d có 28 ngày", thang, nam); //tháng tương ứng có 29 ngày (năm thường)
                }
                break;
            default:
                System.out.printf("Tháng %d có 31 ngày", thang); //những tháng còn lại có 31 ngày
        }
    }

}

Vòng lặp for và for cải tiến

Cú pháp

Vòng lặp for thường được sử dụng khi người dùng biết được số lần thực thi khối lệnh. Nó khá tương tự với vòng lặp while, trong đó khối lệnh trong thân của vòng lặp for được thực thi cho đến khi điều kiện được đề ra là sai. Cũng giống như vòng lặp while, điều kiện được kiểm tra trước khi khối lệnh được thực thi.

Cú pháp sử dụng cho câu lệnh là như sau:

for(khởi_tạo_biến_đếm; điều_kiện; thay_đổi_biến_đếm) {

  // khối_lệnh

}

, trong đó,

khởi_tạo_biến_đếm: Là một biểu thức mà sẽ thiết lập giá trị khởi tạo cho biến đếm, đó là biến điều khiển vòng lặp.

điều_kiện: Làm một biểu thức boolean mà để kiểm tra giá trị của biến điều khiển vòng lặp có thỏa mãn điều kiện đề ra hay không. Nếu điều kiện là đúng thì khối_lệnh tiếp tục được thực thi, ngược lại thì dừng vòng lặp.

thay_đổi_biến_đếm: Bao gồm câu lệnh để thay đổi giá trị của biến đếm trong mỗi lần lặp. Thông thường trong câu lệnh dùng các toán tử tăng giảm như ++, --, hoặc các phép toán gán rút gọn như +=, -=. Không có dấu chấm phẩy (;) ở phía cuối của câu lệnh tăng giảm. Tất cả có ba phần khai báo được phân cách nhau bằng dấu (;).

Hình sau thể hiện luông thực thi của vòng lặp for.

Luông thực thi vòng lặp for

Như thể hiện, việc thực thi vòng lặp bắt đầu với phần khởi tạo (initialization), nói chung phần khởi tạo là một biểu thức dùng để thiết lập giá trị cho biến điều khiển vòng lặp và đóng vai trò như một biến đếm. Biểu thức khởi tạo được thực thi chỉ một lần duy nhất khi vòng lặp bắt đầu được thực thi. Tiếp theo, biểu thức điều kiện (condition) được thực hiện và kiểm tra xem biến điều khiển vòng lặp có thỏa mãn với giá trị đề ra hay không, nếu thỏa mãn thì thực hiện khối lệnh (statements), ngược lại thì thoát khỏi vòng lặp for.

Đoạn mã 1 sau thể hiện việc sử dụng vòng lặp for để hiển thị phép nhân với 10 của từng số từ 1 đến 5.

Đoạn mã 1:

public class PrintMultiplesWithForLoop {

  public static void main(String[] args) {

    int num, product;

    //Vòng lặp for bao gồm 3 thành phần

    for (num = 1; num <= 5; num++) {

      product = num * 10;;

      System.out.printf(“\n % d * 10 = % d “, num, product);

    } //Chuyển điều khiển quay lại vòng lặp for mỗi khi lặp xong

  }

}

Phân tích: Trong phần khởi tạo, biến num được khởi tạo là 1. Câu lệnh điều kiện num <= 5 sẽ đảm bảo rằng vòng lặp được thực thi trừng nào num còn nhỏ hơn hoặc bằng 5. Câu lệnh tăng, num++ sẽ tăng giá trị của num lên 1 sau mỗi lần lặp. Cuối cùng, vòng lặp kết thúc khi điều kiện trở thành sai, tức là khi biến num có giá trị bằng 6.

Output của đoạn mã 1 như sau:

Output của vòng lặp for đơn giản

Phạm vi của biến điều khiển trong vòng lặp for

Phần lớn các biến điều khiển chỉ được dùng trong vòng lặp for và không được sử dụng ở những nơi khác trong chương trình. Do vậy, ta có thể giới hạn phạm vi của nó bằng cách khai báo nó ngay khi khởi tạo giá trị cho nó.

Đoạn mã 2 viết lại vòng lặp for ở đoạn mã 1 trong đó khai báo biến đếm bên trong vòng lặp for.

Đoạn mã 2:

for (int num = 1; num <= 5; num++) {

  //khối_lệnh

}

Sử dụng toán tử dấu phẩy trong vòng lặp for

Vòng lặp for có thể được mở rộng để cho phép có nhiều hơn một biểu thức khởi tạo hoặc biểu thức tăng/giảm bằng cách dùng toán tử (,) để phân cách các biểu thức này. Khi đó việc thực thi các biểu thức sẽ tuân theo thứ tự thực hiện từ trái sang phải. Thứ tự thực hiện này là quan trọng trong trường hợp giá trị của biểu thức thứ hai phụ thuộc vào giá trị tính toán được ở biểu thức thứ nhất.

Đoạn mã 3 cho thấy việc sử dụng vòng lặp để in ra một bảng tính cộng hai biến sử dụng toán tử (,).

Đoạn mã 3:

public class ForLoopWithComma {

  public static void main(String[] args) {

    int i, j;

    int max = 10;  /* Phần khởi tạo và phần tăng/giảm chứa đựng nhiều hơn một câu lệnh */

    for (i = 0, j = max; i <= max; i++, j--) {

      System.out.printf("\n%d + %d = %d", i, j, i + j);

    }

  }

}

Phân tích đoạn mã: 3 biến ij, và max có kiểu nguyên int được khai báo, trong đó biến max ban đầu được gán trá trị là 10. Ở phần khởi tạo của vòng lặp for, i được gán giá trị 0 và j được gán giá trị của max (tức là 10), bạn thấy hai câu lệnh gán này phân cách nhau bằng dấu phẩy (,). Ở phần điều kiện ta có i <= max, tức là vòng lặp for sẽ được thực thi trừng nào giá trị chứa trong biến i còn nhỏ hơn hoặc bằng giá trị chứa trong biến max. Ở phần thứ 3 của vòng lặp for có hai câu lệnh là i++ và j--, điều này có nghĩa sau mỗi lần lặp thì giá trị trong biến i sẽ tằng 1 đơn vị và biến j sẽ giảm 1 đơn vị. Như vậy thì tổng giá trị chứa trong hai biến i và j sẽ luôn bằng nhau và bằng max sau mỗi lần lặp.

Kết quả của đoạn mã trên như sau:

Output của vòng lặp for dùng toán tử dấu phẩy (comma)

Các dạng khác nhau của vòng lặp for

Cấu trúc của vòng lặp for làm cho nó trở lên mạnh mẽ và linh hoạt, cả ba thành phần của vòng lặp for không nhất thiết cứ phải được khai báo và sử dụng chỉ bên trong nó, nghĩa là bạn có thể bỏ qua hoặc để trống bất kỳ phần nào của vòng lặp for.

Đoạn mã 4 dưới đây thể hiện việc sử dụng vòng lặp for nhưng không có phần khởi tạo.

Đoạn mã 4:

public class ForLoopWithNoInitialization {

  public static void main(String[] args) {

    /* Biến đếm num được khai báo và khởi tạo ở ngoài vòng lặp for */

    int num = 1;

    /* Biến boolean flag được khởi tạo là false */

    boolean flag = false;

    /* Vòng lặp for được thực thi với giá trị ban đầu của biến num là 1 và lặp
    * trong khi giá trị của biến flag là not true */

    for (; !flag; num++) {

      System.out.println(“Value of num: “ + num);

      if (num == 5) {

        flag = true;

      }

    } //Kết thúc vòng lặp for

  }

}

Vòng lặp for ở trên sẽ được thực thi cho đến khi flag được đặt là true. Kết quả của đoạn mã là như sau:

Output của vòng lặp for không có phần khởi tạo

Vòng lặp for không xác định

Nếu tất cả ba biểu thức đều trống, thì sẽ dẫn đến một vòng lặp không xác định, lý do là bởi vì không có điều kiện để kiểm soát vòng lặp.

Đoạn mã 5 thể hiện vòng lặp for không xác định.

Đoạn mã 5:

.....

for( ; ; ) {

  System.out.println(“This will go on and on”);

}

.....

Đoạn mã sẽ in ra 'This will go on and on' cho đến khi vòng lặp được ngắt bằng tay. Câu lệnh break có thể được sử dụng để ngắt vòng lặp. Vòng lặp không giới hạn làm cho chương trình chạy trong thời gian dài dẫn đến việc tiêu thụ tất cả các nguồn lực và dừng hệ thống. Vì thế, trong chương trình ta cần phải tránh những vòng lặp dạng như vậy.

Khi giá trị nhập vào của chương trình không được biết trước, thì ta có thể sử dụng loại vòng lặp không xác định trong chương trình, trong đó nó sẽ chờ cho đến khi người dùng nhập liệu. Vì vậy, khi người dùng nhập liệu, thì hệ thống sẽ xử lý đầu vào, sau đó lại bắt đầu thực thi vòng lặp vô hạn.

Vòng lặp for cải tiến

Java SE 5 trở đi đã mở rộng vòng lặp for cơ bản để tăng khả năng đọc của vòng lặp. Phiên bản mới của vòng lặp for thực thi theo kiểu 'for-each', còn được gọi với tên là vòng lặp for cải tiến.

Vòng lặp for cải tiến được thiết kế để truy xuất hoặc lặp thông qua tập hợp các đối tượng như mảng thông thường, ArrayList, LinkedList, HashSet, ... Những lớp này được định nghĩa trong framework tập hợp và được sử dụng để lưu các đối tượng.

Cú pháp sử dụng vòng lặp for cải tiến như sau:

Cú pháp:

for (type var : collection) {

  // khối_lệnh

}

trong đó,

type: là kiểu của tập hợp.

var: là biến lặp dùng để lưu từng phần tử của tập hợp qua mỗi lần lặp. Vòng lặp for cải tiến sẽ lặp từ phần tử đầu đến phần tử cuối của tập hợp, mỗi lần lặp sẽ lấy một phần tử lưu vào biến var.

Sau khi lấy xong phần tử cuối của tập hợp thì vòng lặp sẽ kết thúc.

Đoạn mã 6 sau đây sử dụng vòng lặp for thường để nhập liệu cho mảng chuỗi str và dùng vòng lặp for cải tiến để in ra các phần tử của mảng chuỗi đó.

Đoạn mã 6:

package demo;

import java.util.Scanner;       

public class Demo3 {

  public static void main(String[] args) {

    Scanner input = new Scanner(System.in);

    String[] str = new String[5];

    System.out.println("Nhập liệu các chuỗi:");

    for(int i=0; i<str.length; i++){

      System.out.printf("Chuỗi thứ %d: ",i+1);

      str[i] = input.nextLine();

    }

    System.out.println("Sau khi nhập liệu, ta được:");

    for(String s : str){

      System.out.println(s);

    }

  }

}

Output của đoạn mã trên như sau:

Output vòng lặp for cải tiến

Vòng lặp lồng

Việc đặt một vòng lặp bên trong một vòng lặp khác thì gọi là vòng lặp lồng. Ví dụ, một vòng lặp while có thể được đặt trong vòng lặp do-while và một vòng lặp for có thể được đặt trong một vòng lặp while hoặc một vòng lặp for khác. Khi ta lồng hai vòng lặp, thì vòng lặp ngoài sẽ điều khiển số lần thực thực thi của vòng lặp bên trong. Cụ thể là với mỗi lần lặp của vòng lặp for ngoài thì vòng lặp bên trong sẽ được thực thi cho đến khi phần điều kiện của nó trở thành sai.

Số mức lồng nhau của các loại vòng lặp là không giới hạn. Trong thực tế thì ta hay bắt gặp sự lồng nhau của các vòng lặp for.

Đoạn mã 7 thể hiện việc sử dụng vòng lặp for lồng để hiển thị một mẫu.Code Snippet 10 demonstrates the use of a nested-for loop for displaying a pattern.

Đoạn mã 7:

public class DisplayPattern {

  public static void main(String[] args) {

    int row, col;

    //Vòng lặp for ngoài thực thi 5 lần

    for (row = 1; row <= 5; row++) {

      /* Mỗi lần lặp for ngoài thì vòng lặp for trong sẽ lặp row lần */

      for (col = 1; col <= row; col++) {

        System.out.print(" * ");

      } //Kết thúc vòng lặp for trong

      System.out.println();

    } //Kết thúc vòng lặp for ngoài

  }

}

Phân tích đoạn mã: vòng lặp for ngoài bắt đầu với biến đếm row được khởi tạo giá trị ban đầu là 1. Giá trị trong row sẽ tăng dần qua câu lệnh row++ cho đến khi row>5. Thân của vòng lặp for ngoài chưa vòng lặp for trong, trong đó biến đếm col được khởi tạo giá trị ban đầu là 1. Vòng lặp for trong sẽ được thực thi cho đến khi col>row.

Mỗi khi thực hiện xong vòng lặp for bên trong thì quyền điều khiển lại chuyển ra thành phần thứ 3 của vòng lặp for ngoài để tăng biến đếm row 1 đơn vị, rồi chuyển sang thành phần thứ 2 để kiểm tra điều kiện, nếu điều kiện đúng thì tiếp tục thực hiện vòng lặp for trong.

Output của đoạn mã trên như sau:

 * 
 *  * 
 *  *  * 
 *  *  *  * 
 *  *  *  *  * 

So sánh giữa các loại vòng lặp

Lựa chọn vòng lặp nào vào lập trình là dựa trên thực tế hoặc kinh nghiệm lập trình của bạn. Bảng dưới đây sẽ so sánh các loại vòng lặp với nhau, giúp bạn hiểu rõ hơn về chúng.

while/for do-while
Kiểm tra điều kiện trước, rồi mới thực thi khối lệnh. Thực thi khối lệnh trước, rồi mới kiểm tra điều kiện.
Nếu điều kiện sai ngay từ đầu thi không thực thi. Thực thi ít nhất 1 lần ngay cả khi điều kiện sai ngay từ đầu.

while

Tương tự như vòng lặp for, vòng lặp while cũng được dùng để thực hiện lặp đi lặp lại nhiều lần một khối lệnh. Vòng lặp while còn gọi là vòng lặp không xác định (vô hạn) vì thông thường ta sẽ không xác định được số lần thực hiện khối lệnh trong nó.

Cú pháp:

while(Điều_kiện){
Khối_lệnh;
}

Quy cách thực thi:

Vòng lặp while sẽ kiểm tra Điều_kiện trước:

+ Nếu sai thì thoát khỏi vòng lặp mà không thực hiện Khối_lệnh.

+ Nếu đúng thì thực hiện Khối_lệnh, thực hiện xong lại quay lên kiểm tra Điều_kiện, chu trình thực hiện cứ như vậy cho đến khi Điều_kiện sai.

Ví dụ 1:

Phần điều kiện của vòng lặp while là giá trị true có nghĩa rằng điều kiện là luôn luôn đúng, tức khối lệnh của nó sẽ được thực hiện vô hạn lần.

while(true) {  // vòng lặp sẽ thực hiện khối lệnh vô hạn lần
System.out.println("Lâp trình viên");  //câu lệnh này sẽ được thực hiện vô hạn lần
}

Ví dụ 2:

Ví dụ sau sử dụng một biến đếm n để thực hiện khối lệnh với một số lần mong muốn, điều này có nghĩa ta cũng có thể biến while trở thành vòng lặp xác định.

int n=0;
while(n<10){
System.out.println("Lập trình viên - Programmer - Coder");  //câu lệnh này sẽ được thực hiện 10 lần
n++;
}

Ví dụ 3:

Còn đây là ví dụ áp dụng while để kiểm soát giá trị nhập vào phải thỏa mãn điều kiện đặt ra (validate). Trong trường hợp này bạn sẽ phải nhập một số nguyên n nằm trong khoảng (0<n<100), nếu không hệ thống sẽ bắt bạn nhập lại và sẽ lặp đến khi nào bạn nhập đúng theo yêu cầu thì thôi.

package javaapplication2;

import java.io.IOException;
import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class JavaApplication2 {

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        int n;
        System.out.print("Mời bạn nhập một số n (0<n<1000): ");
        n = sc.nextInt();
        while (n <= 0 || n >= 1000) { //nếu n không nằm trong khoảng 0<n<1000 thì sẽ thực hiện khối lệnh phía dưới
            System.out.print("Mời bạn nhập lại n: ");
            n = sc.nextInt();
        }
    }
}

Xem thêm

do-while

Tương tự như vòng lặp while, vòng lặp do-while cũng dùng để thực hiện lặp đi lặp lại nhiều lần một khối lệnh.

do-while cũng được gọi là vòng lặp không xác định (vô hạn) vì thông thường ta sẽ không xác định được số lần thực hiện khối lệnh trong nó.

Cú pháp:

do{

Khối_lệnh;

}while(Điều_kiện);

Quy cách thực hiện:

Vòng lặp do-while sẽ thực hiện Khối_lệnh trước rồi mới kiểm tra Điều_kiện:

+ Nếu sai thì thoát khỏi vòng lặp mà không thực hiện thêm Khối_lệnh.

+ Nếu đúng thì tiếp tục thực hiện Khối_lệnh, thực hiện xong lại quay lên kiểm tra Điều_kiện, chu trình thực hiện cứ như vậy cho đến khi Điều_kiện sai.

Như vậy, Khối_lệnh trong vòng lặp do-while sẽ được thực hiện ít nhất một lần cho dù Điều_kiện sai ngày từ đầu.

Ví dụ:

do{

System.out.println("Lap trinh vien"); //câu lệnh này sẽ được thực hiện một lần

}while(false); //vì điều kiện sai ngay từ đầu

 

int n=0;

do {

System.out.println("\nLap trinh vien"); //câu lệnh này sẽ được thực hiện 10 lần

n++;

}while(n<10);

Nhập vào một số n (0<n<1000):

package javaapplication2;

import java.io.IOException;
import java.util.Scanner;

/**
 *
 * @author dtlong
 */
public class JavaApplication2 {

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        int n;

        do { //thực hiện khối lệnh trước

            System.out.println("Mời bạn nhập một số n (0<n<1000): ");

            n = sc.nextInt();

        } while (!(n > 0 && n < 1000)); //nếu n không nằm trong khoảng 0-1000 thì thực hiện tiếp khối lệnh
    }
}

Xem thêm

Thread

Giới thiệu

Một tiến trình là một chương trình đang được thực thi. Mỗi tiến trình có các tài nguyên run-time của chính bản thân nó, như dữ liệu, các biến và các không gian bộ nhớ. Những tài nguyên thời gian chạy này tạo thành một môi trường thực thi cho các tiến trình trong một chương trình. Vì vậy, mỗi tiến trình đều có một bộ chứa môi trường thực thi để chạy độc lập. Mỗi tiến trình thực thi một số tác vụ tại một thời điểm và mỗi tác vụ được thực hiện bởi một luồng riêng. Bản thân luồng không là gì cả nhưng nó là đơn vị cơ bản để hệ điều hành cấp phát bộ xử lý thời gian. Mỗi luồng là một thực thể của tiến trình và có thể sắp xếp được thứ tự thực hiện. Tiến trình sẽ bắt đầu với một luồng đơn, thường được gọi là nguyên thủy, mặc định hay luồng chính. Như vậy, tiến trình sẽ lần lượt chứa mộ hoặc nhiều luồng.

Mỗi tiến trình có những đặc điểm sau:

- Có một tập hoàn chỉnh các tài nguyên run-time cơ bản để chạy độc lập.

- Là đơn vị nhỏ nhất có khả năng thực thi mã lệnh trong một ứng dụng để giải quyết một công việc hay một tác vụ cụ thể.

- Một số luồng có thể được thực thi tại cùng một thời điểm, nhằm cho phép việc thực hiện các tác vụ của một ứng dụng cùng lúc.

So sánh tiến trình và luồng

Đa luồng cho phép sử dụng chung một vùng nhớ còn tiến trình thì không thể trực tiếp truy cập bộ nhớ của tiến trình khác.

Một số điểm tương tự và khác nhau giữa tiến trình và luồng:

- Tương tự nhau

+ Các luồng chia sẽ một đơn vị xử lý trung tâm (CPU) và chỉ một luồng được thực thi tại một thời điểm.

+ Các luồng nằm bên trong chuỗi thực thi các tiến trình.

+ Một luồng có thể tạo các luồng con hay các phân luồng.

+ Nếu một luồng nào đó bị khóa thì không ảnh hưởng đến các luồng khác.

- Sự khác nhau

+ Không giống như tiến trình, các luồng là không độc lập nhau.

+ Không giống như tiến trình, tất cả các luồng có thể truy cập các địa chỉ trong tác vụ.

+ Không giống như tiến trình, các luồng được thiết kế để hỗ trợ nhau.

Ứng dụng và sử dụng luồng

Trong Java, một luồng có thể xử lý song song nhiều tác vụ.

Một số ứng dụng của luồng là:

- Chơi nhạc và hiển thị ảnh cùng lúc.

- Hiển thị nhiều ảnh trên màn hình.

- Hiển thị các mẫu văn bản cuộn hoặc các ảnh trên màn hình.

Tạo luồng

Một cách dễ dàng để tạo một luồng mới là tạo một lớp dẫn xuất từ lớp java.lang.Thread. Lớp này bao gồm các phương thức và hàm tạo sẽ trợ giúp trong việc phát hành khái niệm lập trình đồng thời trong Java.

Điều này được dùng để tạo các ứng dụng có thể thực thi cùng lúc nhiều tác vụ tại một thời điểm đã cho. Giải pháp này không thực hiện được khi một lpws muống thực thi luồng được dẫn xuất từ lớp khác.

Dưới đây là các bước tạo một luồng mới từ lớp Thread.

Bước 1: Tạo một lớp con

Bằng cách khai báo một lớp con của lớp Thread được định nghĩa trong gói java.lang.

Ví dụ dưới đây minh họa điều này.

class MyThread extends Thread { //Dẫn xuất từ lớp Thread
  //định nghĩa lớp
  . . .
}

Bước 2: Ghi đè phương thức run()

Bên trong lớp con ta cần ghi đè phương thức run() đã được định nghĩa trong lớp Thread. Đoạn mã trong phương thức run() định nghĩa chức năng được yêu cầu cho luồng để thực thi. Phương thức run() trong luồng cũng tương tự như phương thức main() trong ứng dụng.

Ví dụ sau thể hiện cách thực thi phương thức run().

class MyThread extends Thread {
  public void run() //ghi đè phương thức run()
  {
    //thực thi
  }
  . . .
}

Bước 3: Khởi động luồng

Từ phương thức main() tạo một đối tượng của lớp con, sau đó gọi phương thức start() từ đối tượng này để khởi động Thread. Phương thức start() sẽ đặt đối tượng Thread trong trạng thái sẵn sàng chạy, nó sẽ gọi phương thức run() để cấp phát tài nguyên được yêu cầu để chạy luồng.

Ví dụ:

public class TestThread {
  . . .
  public static void main(String args[]) {
    MyThread t=new MyThread(); //tạo đối tượng thread có tên t
    t.start(); //khởi động thread
  }
}

Các hàm tạo và phương thức lớp Thread

Các hàm tạo của lớp Thread được liệt kê trong bảng 7.1. Lớp ThreadGroup đại diện cho một nhóm các thread và thường được sử dụng trong các hàm tạo của lớp Thread.

Hàm tạo Mô tả
Thread() Hàm tạo mặc định
Thread(Runnable objRun) Tạo một đối tượng Thread, trong đó objRun là đối tượng gọi phương thức run()
Thread(Runnable objRun, String threadName) Tạo một đối tượng Thread mới, trong đó objRun sẽ gọi phương thức run() và threadName là tên của thread sẽ được tạo
Thread(String threadName) Tạo đối tượng Thread mới với tên là giá trị của đối số threadName
Thread(ThreadGroup group, Runnable objRun) Tạo một đối tượng Thread, trong đó group là nhóm thread và objRung là đối tượng gọi phương thức run()
Thread(ThreadGroup
group, Runnable objRun, String threadName)
Tạo một đối tượng Thread trong đó obj sẽ chạy đối tượng đó, threadName là tên của Thread đó và group là nhóm thread mà Thread tham chiếu tới
Thread(ThreadGroup group, Runnable objRun, String threadName, long stackSize) Tạo một đối tượng Thread trong đó obj sẽ chạy đối tượng đó, threadName là tên của Thread đó và group là nhóm thread mà Thread tham chiếu tới, còn stackSize là kích thước stack cụ thể của Thread
Thread(ThreadGroup group, String threadName) Tạo một đối tượng Thread có tên threadName và Thread thuộc nhóm thread có tên group

Các hàm tạo phổ biến của lớp Thread

Lớp Thread cung cấp cho ta cũng cung cấp cho ta khá nhiều phương thức để làm việc với các chương trình. Bảng dưới đây liệt kê các phương thức phổ biến của Thread.

Tên phương thức Mô tả
static int activeCount() Trả về số lượng các thread active và thread hiện tại trong chương trình
static Thread currentThread() Trả về một tham chiếu cuar đối tượng thread đang thực thi
ThreadGroup getThreadGroup() Trả về nhóm thread chứa thread hiện thời
static boolean interrupted() Kiểm tra thread hiện thời xem có bị ngắt không
boolean isAlive() Kiểm trả thread hiện thời còn sống không
boolean isInterrupted() Kiểm tra thread hiện thời đã được ngắt hay chưa
void join() Chờ thread hiện thời die
void setName(String name) Thay đổi tên của thread hiện thời thành name

Các phương thức phổ biến của lớp Thread

Ví dụ sau minh họa việc tạo một luồng mới từ lớp Thread và sử dụng một số phương thức của lớp này.

package demo;

public class NamedThread extends Thread {

    String name;

    @Override
    public void run() {

//Lưu số lượng luồng
        int count = 0;

        while (count <= 3) {

//Hiển thị số lượng luồng
            System.out.println(Thread.activeCount());

//Hiển thị tên của luồng đang chạy
            name = Thread.currentThread().getName();

            count++;

            System.out.println(name);

            if (name.equals("Thread1")) {
                System.out.println("Marimba");
            } else {
                System.out.println("Jini");
            }

        }

    }

    public static void main(String args[]) {

        NamedThread objNamedThread = new NamedThread();

        objNamedThread.setName("Thread1");

//Hiển thị trạng thái của luồng là còn sống hay không
        System.out.println(Thread.currentThread().isAlive());

        System.out.println(objNamedThread.isAlive());

        objNamedThread.start();

        System.out.println(Thread.currentThread().isAlive());

        System.out.println(objNamedThread.isAlive());

    }

}

Giải thích ví dụ:

Trong ví dụ trên, NamedThread được khai báo là một lớp dẫn xuất từ lớp Thread, trong phương thức main() tạo một đối tượng thread có tên là objNamedThread và ta dùng phương thức setName() để đặt tên cho nó là Thread1. Đoạn ma cũng kiểm tra xem thread hiện thời còn sống hay không bằng cách gọi phương thức isAlive() và hiển thị giá trị trả về của phương thức. Ở đây sẽ in ra kết quả là true vì thread chính đã bắt đầu thực thi và hiện tại vẫn còn sống. Đoạn mã trên cũng kiểm tra xem objNamedThread còn sống hay không, và trong trường hợp ví dụ trên thì thì đối tượng này không còn sống vì nó đã không được thực thi. Tiếp theo, phương thức start() được gọi trên đối tượng objNamedThread dẫn đến phương thức run() được kích hoạt, run() sẽ in tổng số lượng các thread đang chạy (lúc này là 2). Sau đó nó kiểm tra tên của thread đang chạy và in ra Marimba nếu thread đang chạy hiện thời có tên là Thread1. Phương thức sẽ tiến hành kiểm tra 3 lần. Output của đoạn mã ví dụ trên sẽ là như sau:
true
false
true
true
2
Thread1
Marimba
2
Thread1
Marimba
2
Thread1
Marimba
2
Thread1
Marimba

Những lưu ý về lập trình đồng thời

- Lập trình đồng thời là một tiến trình chạy cùng một lúc nhiều tác vụ.

- Trong Java thì có thể thực thì ta có thể thực hiện đồng thời một hàm được gọi và các câu lệnh sau lời gọi hàm mà không cần chờ hàm được gọi kết thúc.

- Hàm được gọi sẽ chạy độc lập và đồng thời với lời gọi chương trình, và có thể chia sẻ các biến, dữ liệu, ... với nó.

Giao diện Runnable

Interface Runnable được thiết kế để cung cấp một tập phổ biến các quy tắc cho các đối tượng muốn thực thi một đoạn mã nào đó trong khi một thread đang active. Hay nói một cách khác của việc tạo thread mới là bằng cách thực thi giao diện  interface is designed to provide a common set of rules for objects that wish to execute a code while a thread is active. Another way of creating a new thread is by implementing the Runnable interface. This approach can be used because Java does not allow multiple class inheritance. Therefore, depending upon the need and requirement, either of the approaches can be used to create Java threads.

Các bước để tạo và chạy một Thread mới bằng giao diện Runnable như sau:

- Bước 1: Thực thi giao diện Runnable

Khai báo một lớp thực thi giao diện. Ví dụ:

class MyRunnable implements Runnable {
. . .
}

- Bước 2: Thực thi phương thức run().

class MyRunnable implements Runnable {

  public void run() {// ghi đè phương thức run()
    . . . // code thực thi

  }

}

- Bước 3: Kích hoạt Thread

Kích hoạt bằng cách dùng phương thức start().

class ThreadTest {

  public static void main(String args[]) {

    MyRunnable r=new Runnable();

    Thread thObj=new Thread(r);

    thObj.start(); //kích thoạt thread

  }

}

Ví dụ sau minh họa cách tạo và sử dụng thread bằng cách sử dụng giao diện Runnable.

package test;

class NamedThread implements Runnable {

  /* Biến này lưu tên của luông */

  String name;

  public void run() {

    int count = 0; //biến lưu số lượng luồng

    while(count < 3) {

      name = Thread.currentThread().getName();

      System.out.println(name);

      count++;
    }

  }

}

public class Main {

  public static void main(String args[]) {

    NamedThread objNewThread= new NamedThread()

    Thread objThread = new Thread(objNewThread);

    objThread.start();
  }

}

Trong ví dụ trên, lớp NamedThread thực thi interface Runnable, do đó thể hiện của nó có thể được truyền như là đối số tới hàm tạo của lớp Thread. Output của ví dụ trên như sau:

Thread-0
Thread-0
Thread-0

Các trạng thái của luồng

Mỗi luồng đều có một số trạng thái như new, alive, runnable, blocked, waiting  terminated, và luồng có thể ở những trạng thái khác nhau trong chương trình. Mặc định khi tạo mới luồng thì luồng đó ở trạng thái không sống alive.

Khi ở trạng thái không sống thì nó là luồng empty và không được cấp phát tài nguyên hệ thống. Do vậy, trạng thái của luồng được coi là new cho tới khi phương thức start() được goi. Một khi luồng ở trạng thái new thì ta chỉ có thể start hoặc stop nó, cho nên lời gọi tới bất kỳ phương thức nào trước start() đều phát sinh ngoại lệ ThreadStateException.

Trạng thái Runnable

Luồng mới có thể có trạng thái runnable khi nó gọi phương thức start(), khi đó luồng trong trạng thái đang sống.

Các luồng có thể được thiết lập thứ tự ưu tiên do trong một hệ thống đơn nhiệm thì không thể thực thi các luồng runnable cùng lúc mà chỉ có 1 luồng runnable được chạy. Một khi ở trạng thái runnable thì luồng đủ điều kiện để chạy, nhưng nó có thể không chạy được dó nó phụ thuộc vào thứ tự ưu tiên luồng (vì vậy trạng thái này mới có tên là runnable). Khi luồng chạy thì nó sẽ thực thi các câu lệnh trong phương thức run().

http://v1study.com/public/images/article/java-thread-runnable-state.png
Trạng thái Runnable

Ví dụ:

. . .

MyThreadClass myThread = new MyThreadClass();
myThread.start();
. . .

Tạo một luồng và gọi phương thức start() của luồng đó, luồng sẽ ở trạng thái runnable.

Trạng thái khóa (Blocked State)

Đây là một trong các trạng thái của thread:

- Nó còn sống nhưng hiện tại không đủ điều kiện để chạy

- Mặc dù không có khả năng chạy nhưng nó có thể quay trở về trạng thái chạy (runnable) sau khi nhận màn hình hoặc khóa.

Thread ở trạng thái này sẽ chờ hoạt động trên tài nguyên hoặc đối tượng ở lần kế tới đang được xử lý bởi một thread khác. Thread đang chạy sẽ chuyển trạng thái thành khóa khi một trong các phương thức sleep(), wait() hoặc suspend() được gọi.

Trạng thái Waiting

Luồng có sẽ ở trạng thái này khi nó đang chờ luồng khác giải phóng tài nguyên cho nó. Khi hai hoặc nhiều luồng chạy đồng thời và chỉ một luồng chiếm giữ tài nguyên thì các luồng khác sẽ phải cho cho đến khi luồng này giải phóng tài nguyên. Trong trạng thái chờ này thì luồng sẽ sống nhưng không chạy.

Luồng sẽ ở trạng thái waiting khi nó gọi phương thức wait(). Lời gọi tới phương thức notify() hay notifyAll() sẽ chuyển luồng từ trạng thái waiting sang runnable.

Trạng thái Terminated

Sau khi luồng thực thi phương thức run() sx sẽ die và chuyển sang trạng thái ngắt (terminated). Đây là cách dừng (stop) thông thường của một luồng. Khi luồng bị ngắt thì nó không thể quay về trạng thái runnable. Các phương thức chuyển sạng trạng thái terminated của luồng là stop() và destroy().

Lưu ý - Nếu luồng đang ở trạng thái terminated mà nó lại gọi phương thức start() thì nó sẽ ném ra một ngoại lệ run-time.

Các phương thức của lớp Thread

Lớp Thread có chứa một số phương thức có thể được dùng để thao tác với luồng.

Phương thức getName()

Phương thức này lấy tên của luồng hiện thời.

Ví dụ:

public void run() {

for(int i = 0;i < 5;i++) {

Thread t = Thread.currentThread();

System.out.println(“Name = “ + t.getName());
...

The code snippet demonstrates the use of getName() method to obtain the name of the thread object.

Lưu ý - Phương thức setName(String name) của luồng có tác dụng thiết lập lại tên cho luồng. Tên được truyền qua đối số của phương thức.

Phương thức start()

Phương thức này được gọi sẽ chuyển trạng thái của luồng thành alive, và nó sẽ gọi phương thức run() của luồng.

Tại thời điểm gọi phương thức start() thì:

- Bắt đầu thực thi luồng mới

- Chuyển luồng sang trạng thái runnable

Ví dụ:

. . .
NewThread thObj = new NewThread():
thObj.start();
. . .

Lưu ý - Chỉ gọi phương thức start() của luồng một lần, vì nó không thể được khởi động lại sau mỗi lần thực thi.

Phương thức run()

Phương thức này sẽ kích hoạt thread, thread sẽ chính thức hoạt động. Phương thức này có một số đặc điểm sau:

- Có tầm vực là public

- Không cần thiết phải có đối số

- Không trả về giá trị

- Không ném ngoại lệ

Phương thức này sẽ hoạt động mỗi khi phương thức start() được gọi.

Ví dụ:

class myRunnable implements Runnable {
. . .
public void run() {
System.out.println(“Inside the run method.”);
}
. . .
}

Phương thức sleep()

Phương thức này có những đặc điểm sau:

- Nó treo luồng hiện tại không có thực thi

- Tạo bộ xử lý thời gian cho phép các luồng khác nhau của một hoặc nhiều ứng dụng có thể chạy trên hệ thống máy tính

- Nó sẽ dừng thực thi nếu luồng active trong một khoảng thời gian cụ thể theo đơn vị mili giây hoặc nano giây

- Nó kích hoạt ngoại lệ InterruptedException khi nó bị ngắt bởi phương thức interrupt()

Ví dụ:

try {
  myThread.sleep (10000);
}
catch (InterruptedException e)
{}

Trong đoạn mã trên, myThread sẽ chuyển sang trạng thái sleep khoảng thời gian 10 giây.

Phương thức interrupt()

Phương thức này dùng để ngắt luồng. Phương thức này có những đặc điểm sau:

- Luồng đã bị ngắt có thể die, nó sẽ chờ một tác vụ khác hoặc chuyển tới bước tiếp theo tùy thuộc vào yêu cầu của ứng dụng.

- Phương thức này không ngắt hoặc dùng luồng đang chạy, thay vào đó nó sẽ ném ngoại lệ InterruptedException nếu luồng bị khóa để thoát khỏi trạng thái khóa.

- Nếu luồng bị khóa do phương thức wait(), join() hay sleep() thì nó sẽ nhận một ngoại lệ InterruptedException để chấm dứt phương thức đó.

Lưu ý - Luồng có thể tự ngắt. Khi một luồn không tự ngắt được thì phương thức checkAccess() sẽ được gọi, phương thức này sẽ kiểm tra xem luồng hiện tại có quyền thay đổi hay không. Trong trường hợp không có quyền thì trình quản lý bảo mật sẽ ném một ngoại lệ SecurityException thông báo vi phạm bảo mật.

Quản lý luồng

Vì các luồng không thể thực hiện cùng một thời điểm, cho nên ta cần phải sắp xếp thứ tự ưu tiên sao cho luồng nào quan trọng hơn thì được thực hiện trước, và luôn cần đảm bảo rằng không xảy ra hiện tượng xung đột luồng, tức là các luồng cần phải hoạt động tuần tự và trôi chảy.

Tầm quan trọng của mức ưu tiên luồng

Khi tạo các ứng dụng đa luồng thì có thể xảy ra trường hợp luồng này đang chạy thì ta muốn chạy một luồng khác có tầm quan trọng hơn, muốn làm được điều này thì ta cần thông qua mức ưu tiên luồng.

Các loại mức ưu tiên luồng

Có 3 loại mức yêu tiên luồng như sau:

- Thread.MAX_PRIORITY

Mức này tương đương với 10, mức cao nhất.

- Thread.NORM_PRIORITY

Mức này tương đương 5, mức mặc định.

- Thread.MIN_PRIORITY

Mức này tương được với 1, mức thấp nhất.

Phương thức setPriority(newPriority)

Phương thức này thiết lập mức ưu tiên cho luồng. Đối số newPriority của nó có thể nhận một số nguyên từ 1 đến 10.

Ví dụ sau thể hiện cách sử dụng phương thức này:

. . .
Thread threadA = new Thread("Meeting deadlines");
threadA.setPriority(8);
. . .

Phương thức getPriority()

Phương thức này dùng để truy xuất đến giá trị ưu tiên hiện tại của bất kỳ thread nào.

Cú pháp:

public final int getPriority()

Luồng Daemon

Luồng daemon có đặc điểm là nó chạy liên tục để thực hiện một dịch vụ mà không cần kết nối nào tới toàn bộ tình trạng của chương trình. Những luồng chạy mã lệnh hệ thống là ví dụ về luồng daemon.

Các đặc điểm của luồng daemon:

- Làm việc trong nền tảng cung cấp của các luồng khác.

- Phụ thuộc hoàn toàn vào các luồng của người dùng.

- JVM đã dừng luồng nào thì luồng đó die nhưng luồng daemon sẽ vẫn còn sống.

Các phương thức liên quan:

- setDaemon(boolean value)

Phương thức này dùng để chuyển một luồng người dùng thành luồng daemon, đối số value của nó chứa giá trị true thì có nghĩa nó sẽ thiết lập thành luồng daemon. Mặc định thì tất cả các luồng đều là luồng người dùng.

- isDaemon()

Phương thức này để kiểm tra xem luồng có phải là daemon hay không, trả về true là đúng, false là sai.

Lưu ý: Luồng garbage collector và luồng xử lý sự kiện chuột là những luồng daemon.

Tầm quan trọng của luồng Daemon

Luồng Daemon có thể làm được:

- Các luồng Daemon là những nhà cung cấp dịch vụ cho các luồng khác chạy trong cùng một tiến trình.

- Các luồng Daemon được thiết kế ở dạng các luồng nền mức thấp để thực hiện một số tác vụ như sự kiện chuột.

Bài tập phần thừa kế

Bài tập 1:

Câu 1:

Tạo lớp Person chứa thông tin

- Tên

- Giới tính

- Ngày sinh

- Địa chỉ

Với đầy đủ hàm get set, constructor không tham số, constructor đầy đủ tham số

1. Viết phương thức inputInfo(), nhập thông tin Person từ bàn phím

2. Viết phương thức showInfo(), hiển thị tất cả thông tin Person

Câu 2:

Tạo lớp Student thừa kế Person, lưu trữ các thông tin một sinh viên

- Mã sinh viên: chứa 8 kí tự

- Điểm trung bình: từ 0.0 – 10.0

- Email: phải chứa kí tự @ và không tồn tại khoảng trắng

1. Override phương thức inputInfo(), nhập thông tin Student từ bàn phím

2. Override phương thức showInfo(), hiển thị tất cả thông tin Student

3. Viết phương thức xét xem Student có được học bổng không? Điểm trung bình trên 8.0 là được học bổng

Câu 3:

Tạo lớp StudentTest, chứa Main kiểm tra chức năng lớp Student

Tạo Menu chọn như sau

a. Chọn 1: Nhập vào n sinh viên (n là số lượng sinh viên, được nhập từ bàn phím)

b. Chọn 2: Hiển thị thông tin tất cả các sinh viên ra màn hình

c. Chọn 3: Hiển thị sinh viên có điểm trung bình cao nhất và sinh viên có điểm trung bình thấp nhất

d. Chọn 4: Tìm kiếm sinh viên theo mã sinh viên. Nhập vào mã sinh viên. Nếu tồn tại sinh viên

có mã đó thì in ra màn hình thông tin sinh viên. Nếu không tồn tại thì in ra: Không có sinh

viên nào có mã là <giá trị của mã sinh viên>

e. Chọn 5: Hiển thị tất cả các sinh viên theo thứ tự tên trong bảng chữ cái (A->Z)

f. Chọn 6: Hiển thị tất cả các sinh viên được học bổng, và sắp xếp theo thứ tự điểm cao xuống thấp

g. Chọn 7: Thoát

Câu 4:

Tạo lớp Teacher, kế thừa từ Person, lưu trữ thông tin một giảng viên

- Lớp dạy (phải bắt đầu bằng 1 trong các chữ: G, H, I, K, L, M)

- Lương một giờ dạy

- Số giờ dạy trong tháng

1. Override phương thức inputInfo(), nhập thông tin Teacher từ bàn phím

2. Override phương thức showInfo(), hiển thị tất cả thông tin Teacher

3. Viết phương thức tính lương thực nhận, trả về lương thực nhận theo công thức:

Nếu lớp dạy là lớp buổi sáng và chiều (Giờ G, H, I, K) thì

Lương thực nhận = lương một giờ dạy * số giờ dạy trong tháng;

Nếu lớp dạy là lớp buổi tối (Giờ L, giờ M) thì

Lương thực nhân = lương một giờ dạy * số giờ dạy trong tháng + 200000đ;

Câu 5:

Tạo lớp TeacherTest, chứa hàm Main kiểm tra chức năng của Teacher

Tạo menu lựa chọn như sau:

a. Chọn 1: Nhập vào n giảng viên (n là số lượng sinh viên, được nhập từ bàn phím)

b. Chọn 2: Hiển thị thông tin tất cả các giảng viên ra màn hình

c. Chọn 3: Hiển thị giảng viên có lương cao nhất

d. Chọn 4: Thoát

Bài tập 2: HỆ THỐNG QUẢN LÝ SỞ THÚ

1. Tạo lớp có tên Animal gồm các thuộc tính và phương thức:

· String Ten

· int Tuoi

· ​String MoTa

· void xemThongTin() //hiển thị tên, tuổi và mô tả của động vật

· abstract void tiengKeu()

2. Tạo một số hàm tạo cho lớp Animal như sau:

· 0 tham số

· 1 tham số (Ten),

· 2 tham số (Ten, Tuoi),

· 3 tham số (Ten, Tuoi, MoTa).

3. Tạo các lớp Tiger, Dog, Cat theo các yêu cầu sau:

  • Thừa kế từ lớp Animal
  • Ghi đè phương thức tiengKeu() để thể hiện những tiếng kêu đặc trưng của từng loài vật
  • Thực thi các hàm tạo sử dụng từ khóa super

4. Tạo lớp có tên Chuong gồm:

· int maChuong

· ​ArrayList AnimalList

· void themConVat(Animal a) //thêm một con vật vào AnimalList

· void xoaConVat(String ten) //xóa con vật có tên tương ứng khỏi AnimalList

5. Tạo lớp có tên Zoo gồm:

· ArrayList DanhSachChuong

· void themChuong(Chuong c) //thêm chuồng vào DanhSachChuong

· void xoaChuong(int machuong) //xóa chuồng có mã tương ứng khỏi DanhSachChuong

6. Tạo lớp có tên TestZoo chứa phương thức main() để quản lý sở thú theo dạng Menu như sau:

  1. Thêm chuồng
  2. Xóa chuồng
  3. Thêm con vật
  4. Xóa con vật
  5. Xem tất cả các con vật
  6. Thoát

7. Khi người dùng chọn 3 thì yêu cầu người dùng nhập vào loại con vật muốn thêm (Tiger, Dog, Cat) rồi nhập các thông tin của con vật và thêm vào AnimaList.

8. Khi người dùng chọn 5 thì hiển thị thông tin cùng tiếng kêu của từng con vật trong sở thú.

Bài tập phần Abstract class & Interface

Bài tập 1: HỆ THỐNG QUẢN LÝ SỐ ĐIỆN THOẠI

1. Tạo một lớp có tên Phone chứa những phương thức trừu tượng sau đây:

- abstract void insertPhone(String name, String  phone)

- abstract void removePhone(String name)

- abstract void updatePhone(String name, String newphone)

- abstract void searchPhone(String name)

- abstract void sort()

2. Tạo lớp có tên PhoneBook thừa kế lớp Phone:

- Tạo một ArrayList tên PhoneList để lưu dữ liệu.

- Phương thức insertPhone(String name, String  phone):

   Nếu tên người dùng (name) chưa có sẵn trong PhoneList thì thêm người dùng cùng số điện thoại (phone) tương ứng vào

   Nếu tên người dùng đã có sẵn thì kiểm tra xem số điện thoại (phone) có khác so với số đã có không, nếu khác thì thêm vào sau số đã có theo dạng như ví dụ sau:

   "0912333333 : 0902345671"

- Phương thức removePhone(String name):

   Xóa người dùng cùng các số điện thoại của chủ sở hữu có tên (name) tương ứng khỏi PhoneList.

- Phương thức updatePhone(String name, String newphone):

   Thay số điện thoại cũ bằng số điện thoại (newphone) mới.

- Phương thức searchPhone(String name):

   Tìm kiếm số điện thoại theo tên người dùng.

- Phương thức sort():

   Sắp xếp các phần tử trong PhoneList theo tên người dùng.

3. Tạo lớp ManagePhoneBook chứa phương thức main() để quản lý chương trình theo dạng Menu như sau:

   PHONEBOOK MANAGEMENT SYSTEM

   1. Insert Phone

   2. Remove Phone

   3. Update Phone

   4. Search Phone

   5. Sort

   6. Exit

Bài tập 2:

1. Tạo một giao diện (interface) có tên INews bao gồm phương thức void Display().

2. Tạo một lớp có tên News:

a. Khai báo các thuộc tính bao gồm: ID (int), Title (String), PublishDate (String), Author (String), Content (String) và AverageRate (float). Tạo các phương thức setter và getter cho từng thuộc tính, riêng AverageRate thì chỉ có getter.

b. Thực thi giao diện INews.

c. Phương thức Display() sẽ in ra Title, PublishDate, Author, Content và AverageRate của tin tức ra console.

d. Khai báo một mảng có tên RateList kiểu int gồm 3 phần tử.

e. Tạo một phương thức có tên Calculate() để thiết đặt thuộc tính AverageRate ở ý a là trung bình cộng của 3 phần tử của mảng RateList ở ý d.

3. Tạo một menu lựa chọn gồm các mục sau:

   1. Insert news
   2. View list news
   3. Average rate
   4. Exit

4. Nếu người dùng chọn 1 từ bàn phím thì:

a. Tạo một thể hiện của lớp News và nhập giá trị cho các thuộc tính Title, PublishDate, Author, Content sau đó yêu cầu người dùng nhập vào 3 đánh giá để lưu vào RateList.

b. Tạo một ArrayList (chỉ tạo duy nhất một ArrayList) để lưu thể hiện của lớp News.

5. Nếu người dùng chọn 2 từ bàn phím thì lấy từng thể hiện trong ArrayList ra và thực thi phương thức Display() tương ứng của từng thể hiện.

6. Nếu người dùng chọn 3 từ bàn phím thì thực hiện tương tự như mục 5 ở trên nhưng trước khi thực thi phương thức Display() thì cần thực thi phương thức Calculate() để tính đánh giá trung bình.

7. Trường hợp người dùng chọn 4 thì sẽ thoát khỏi chương trình.

BÀI TẬP 3: BOOKS MANAGEMENT SYSTEM

In this exam, you’ll have to create a Books Management system. The system allows input, list, search … books.

1. Create an interface name IBook contains this method:

   · void Display()

2. Create a class name Book:

a. Properties

   · ID (int)

   · Name (String)

   · PublishDate (String)

   · Author (String)

   · Language (String)

   · AveragePrice (float) – Read only property

b. Implements the IBook interface in step 1

c. The method Display will print all Name, PublishDate, Author, Language and AveragePrice of the book to the console

d. Declare an array name PriceList type int has size of 5 elements

f. Create a method named Calculate to set AveragePrice = average of 5 int elements in PriceList array.

3. Display  a tasks menu to choose:

   1. Insert new book
   2. View list of books
   3. Average Price
   4. Exit

4. If user type from keyboard then:

a. Create a new Book  instance and input Name, PublishDate, Author, Language and then ask user to enter 5 prices and set to the instance indexer

   - The ID is auto increament ( ID++ )

b. Create an ArrayList<Book> to keep the Book instance in step a

5. If user type 2 from keyboard then:

Loop from all book instances in the ArrayList<Book> then executes the Display method from IBook interface.

6. If user type 3 from keyboard then:

Loop from all book instances in the ArrayList<Book>  then executes the Calculate method and then execute the Display method.

7. While user not chooses Exit (type 4 from keyboard) then go back to the menu step 3 to ask user chooses an option.

BÀI TẬP 4: MARKS MANAGEMENT SYSTEM

1. Create an interface name IStudentMark  contains these properties and methods:

Methods:

void Display()

2. Create a class name StudentMark:

a. Properties:

- FullName (String)

- ID (int)

- Class (String)

- Semester (int)

- AverageMark (float) – Read only property

b. Implements the IStudentMark interface in step 1.

c. The method Display() will print all FullName, ID, Class, Semester, AverageMark.

d. Declare an array name SubjectMarkList type int has size of 5 elements.

e. Create an indexer uses the array SubjectMarkList in step 2c.

f. Create a method named AveCal to set AverageMark = average of 5 int elements in SubjectMarkList array.

3. Display a tasks menu to choose:

menu-marks-management-system

4. If user type from the keyboard then:

a. Create a new StudentMark  instance and input FullName, Class, Semester and then ask users to enter 5 subjectmarks and set to the instance indexer. The ID is auto increament (++ID ).

insert-marks-management-system

b. Create a ArrayList (create only one ArrayList) to keep the StudentMark instance in step 4a.

5. If user type 2 from the keyboard then:

Loop from all studentmark instances in the ArrayList then executes the Display() method from IStudentMark interface.

6. If user type 3 from the keyboard then:

Loop from all studentmark instances in the ArrayList then executes the AveCal() method and then executes the Display() method.

7. While user not chooses Exit (type 4 from the keyboard) then go back to the menu step 3 to ask user chooses an option.

BÀI TẬP 5: STUDENTS MANAGEMENT SYSTEM

In this exam, you’ll have to create a Students Management system. The system allows input, list, search students.

1. Create an interface name IStudent contains these properties and methods:

Methods:

void Display()

2. Create a class name Student:

a. Properties:

- FullName (String)

- ID (int)

- DateofBirth (String)

- Native (String)

- Class (String)

- PhoneNo (String)

- Mobile (int)

b. Implements the IStudent interface in step 1.

c. The method Display() will print all FullName, ID, DateofBirth, Native, Class, PhoneNo and Mobile of the student to the console.

3. Display  a tasks menu to choose:

   1. Insert new Student

   2. View list of Students

   3. Search Students

   4. Exit

4. If users type 1 from the keyboard then:

a. Create a new Student instance and inputs FullName, DateofBirth, Native, Class, PhoneNo, and Mobile. The ID is auto increament (++ID ).

b. Create a ArrayList (create only one ArrayList only) to keep the Student instance in step 4a.

5. If users type 2 from the keyboard then:

Loop from all Student instances in the ArrayList then executes the Display() method from IStudent interface.

6. If users type 3 from the keyboard then:

Loop from all instances of Student class in the ArrayList then search students of class that is inputed from the keyboard and then executes the Display() method.

7. While user does not choose Exit (type 4 from the keyboard) then go back to the menu step 3 to ask user chooses an option.

Nhập liệu bằng Scanner trong Java

In các số từ 0 đến 100 dùng vòng lặp for

Xác định tính nguyên tố

Cách khai báo biến trong Java

Xác định chính phương

Xác định số nguyên hay thực

Tìm số nguyên tố trong một khoảng

In ra các số chẵn chia hết cho 3

Giải phương trình bậc nhất

Giải phương trình bậc hai

Nguyên âm, phụ âm hay ký số

Cách tạo lớp và đối tượng

Cách tạo các getter và setter

Cách định nghĩa hàm tạo

Cách sử dụng các getter và setter

Cách tạo mảng

Cách nhập liệu cho mảng

Cách tìm Max và Min trong mảng

Cách sắp xếp mảng

Xóa phần tử có chỉ số cụ thể khỏi mảng

Xóa phần tử chứa giá trị cụ thể khỏi mảng

Cách dùng ArrayList và viết chương trình dạng Menu

Giới thiệu I/O Stream

Một I/O Stream đại diện cho một nguồn đầu vào hoặc một điểm đầu ra. Một stream có thể đại diện cho nhiều loại nguồn và các điểm đầu ra khác nhau, bao gồm cả các tập tin đĩa, các thiết bị, các chương trình khác, và mảng bộ nhớ.

Stream hỗ trợ nhiều loại dữ liệu khác nhau, bao gồm các byte đơn giản, các kiểu dữ liệu nguyên thủy, các ký tự địa phương theo vùng, và các đối tượng. Một số stream đơn thuần chỉ là truyền dữ liệu; những stream khác thao tác và chuyển đổi dữ liệu trong những cách hữu ích.

Tất cả các stream trình bày các mô hình đơn giản cùng với các chương trình sử dụng chúng: Một stream là một chuỗi các dữ liệu. Một chương trình sử dụng một input stream để đọc dữ liệu từ một nguồn, một mục tại một thời điểm:

Đọc thông tin vào một chương trình.
Đọc thông tin vào một chương trình.

Một chương trình sử dụng một output để ghi dữ liệu tới đích, một mục tại một thời điểm:

Viết thông tin từ một chương trình.
Ghi thông tin từ một chương trình.

Stream có thể xử lý tất cả các loại dữ liệu, từ giá trị nguyên thủy đến các đối tượng cao cấp.

Các nguồn dữ liệu và dữ liệu đích ở hình trên có thể lưu giữ, tạo, hoặc sử dụng dữ liệu. Rõ ràng điều này bao gồm các tập tin đĩa, nhưng một nguồn hoặc đích cũng có thể là một chương trình khác, một thiết bị ngoại vi, một ổ cắm mạng, hoặc một mảng.

Trong các bài viết tiếp theo chúng tôi sẽ sử dụng các loại cơ bản nhất của stream, byte stream để thể hiện các hoạt động phổ biến của Stream I/O. Đối với đầu vào mẫu, chúng tôi sẽ sử dụng tập tin ví dụ xanadu.txt , trong đó có nội dung sau đây:

In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.

Byte Stream

Chương trình sử dụng các byte stream để thực hiện input và output các byte 8-bit. Tất cả các lớp byte steam có nguồn gốc từ InputStream và OutputStream.

Có rất nhiều lớp byte stream. Để minh họa cách thức làm việc của byte stream, chúng ta sẽ tập trung vào các tập tin I/O byte stream là FileInputStream và FileOutputStream. Các loại khác của byte stream được sử dụng trong nhiều cách tương tự nhau; chúng khác nhau chủ yếu ở cách chúng được xây dựng.

Cách dùng Byte Stream

Chúng ta sẽ khám phá FileInputStream và FileOutputStream bằng cách kiểm tra một chương trình ví dụ dưới đây có tên là CopyBytes, trong đó sử dụng byte stream để sao chép tập tin xanadu.txt, mỗi lần sao chép một byte.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
  public static void main (String [] args) throws IOException { 

    FileInputStream in = null;
    FileOutputStream out = null;
    int c;

     try {
       in = new FileInputStream ( "xanadu.txt");
       out = new FileOutputStream ( "outagain.txt");

       while ((c = in.read ()) != -1) {
         out.write (c) ;
       }
     } finally {
       if (in != null) {
         in.close ();
       }
       if(out != null) {
         out.close ();
       }
     }
  }
}

Lớp CopyBytes dành phần lớn thời gian của nó trong một vòng lặp đơn giản để đọc dòng đầu vào và ghi dòng đầu ra, từng byte tại mỗi thời điểm như thể hiện trong hình dưới đây.

Simple dòng byte đầu vào và đầu ra.
Byte Strem input và output đơn giản.

Luôn luôn đóng Stream

Đóng stream khi nó không còn cần thiết là rất quan trọng, quan trọng đến nỗi CopyBytes sử dụng khối finally để đảm bảo rằng cả hai stream sẽ được đóng cho dù có lỗi xảy ra hay không. Việc này cũng giúp tránh rò rỉ tài nguyên nghiêm trọng.

Một lỗi có thể là CopyBytes đã không thể mở một hoặc cả hai tập tin. Khi điều đó xảy ra, biến stream tương ứng với các tập tin không bao giờ thay đổi giá trị null được khởi tạo từ đầu. Đó là lý do tại sao CopyBytes đảm bảo rằng mỗi biến stream chứa một tham chiếu đối tượng trước khi gọi phương thức close().

Khi không sử dụng Byte Streams

CopyBytes có vẻ giống như một chương trình bình thường, nhưng nó thực sự đại diện cho một loại I/O ở mức thấp mà bạn nên tránh. Vì xanadu.txt chứa dữ liệu ký tự, nên cách tốt nhất là sử dụng character stream được trình bày ở bài viết tiếp theo. Ngoài ra còn có stream với nhiều loại dữ liệu phức tạp hơn. Byte Stream chỉ nên được sử dụng cho I/O nguyên thủy nhất.

Nhưng, tại sao ta lại nói về byte stream? Bởi vì tất cả các loại stream khác được xây dựng dựa trên byte stream.

Nguồn: https://docs.oracle.com/javase/tutorial/essential/io/bytestreams.html

Character Stream

Nền tảng Java lưu trữ các giá trị ký tự sử dụng định dạng Unicode. Ký tự dòng I/O sẽ tự động dịch định dạng nội bộ thành và từ các bộ ký tự của địa phương. Đối với miền địa phương Tây, các ký tự nội bộ thường là một tập con của bảng mã ASCII 8-bit.

Đối với hầu hết các ứng dụng, I/O với character stream phức tạp hơn I/O với byte stream. Input và output được thực hiện với các lớp stream tự động dịch thành và từ các bộ ký tự của địa phương. Một chương trình có sử dụng các character stream vị trí của byte stream tự động điều chỉnh thành tập các ký tự địa phương và sẵn sàng cho quốc tế hóa - tất cả mà không cần nhiều nỗ lực từ các lập trình viên.

Nếu quốc tế hóa không phải là một ưu tiên thì ta chỉ có thể sử dụng các lớp character stream mà không cần chú ý nhiều đến các vấn đề về bộ ký tự. Còn nếu quốc tế hóa trở thành một ưu tiên thì chương trình của ta có thể được điều chỉnh mà không cần thu âm rộng. Xem Internationalization đường mòn để biết thêm thông tin.

Cách dùng Streams Character

Tất cả các lớp character stream có nguồn gốc từ Reader và Writer. Cùng với byte stream, ta có các lớp character stream chuyên về I/O file là FileReader và FileWriter. Ví dụ dưới đây minh họa các lớp này thông qua định nghĩa một lớp có tên CopyCharacters.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException; 

public class CopyCharacters {
  public static void main (String [] args) throws IOException { 

  FileReader inputStream = null;
  FileWriter outputStream = null; 

  try {
    inputStream = new FileReader ( "xanadu.txt");
    outputStream = new FileWriter ( "characteroutput.txt"); 

    int c;
    while ((c = inputStream.read ()) != -1) {
      outputStream.write (c) ;
    }
  } finally {
      if (inputStream != null) {
        inputStream.close ();
      }
      if (outputStream != null) {
        outputStream.close ();
      }
    }
  }
}

Lớp CopyCharacters rất giống với CopyBytes. Sự khác biệt quan trọng nhất là CopyCharacters sử dụng FileReader và FileWriter cho đầu vào và đầu ra trong khi CopyBytes sử dụng FileInputStream FileOutputStream. Chú ý rằng cả CopyBytes và CopyCharacters sử dụng một biến int để đọc và ghi. Tuy nhiên, trong CopyCharacters, biến int lưu một giá trị kiểu ký tự với kích thước 16 bit, còn ở CopyBytes, biến int chứa giá trị là một byte có kích thước 8 bit.

Character Stream sử dụng Byte Stream

Character stream được coi là "wrappers" ("bao ngoài") byte stream. Các character stream sử dụng các byte stream để thực hiện các thao tác I/O vật lý trong quá trình xử lý dịch giữa các ký tự và byte. Ví dụ như FileReader sử dụng FileInputStream , trong khi FileWriter sử dụng FileOutputStream.

Có hai stream được coi là "cầu nối" cho việc dịch byte thành ký tự và ngược lại, đó là InputStreamReader và OutputStreamWriter. Chúng được sử dung để tạo ra các character stream khi không có sẵn lớp character stream nào đáp ứng được nhu cầu của ta. Các bài viết socket lessionnetworking trail cho ta thấy cách tạo ra các character stream từ các byte stream được cung cấp bởi các lớp socket.

Line-Oriented I/O

I/O character thường xảy ra ở các đơn vị lớn hơn các ký tự đơn. Đơn vị phổ biến ở đây dòng (line), đó là một chuỗi ký tự với kết thúc là ký tự kết thúc chuỗi, ký tự này có thể là một chuỗi carriage-return/line-feed"\r\n" ), một carriage-return"\r" ), hoặc một line-feed"\n" ). Việc hỗ trợ tất cả các ký tự này có thể cho phép các chương trình đọc các tập tin văn bản được tạo ra trên bất kỳ hệ điều hành nào.

Bây giờ ta thay đổi lớp CopyCharacters thông qua việc sử dụng I/O line-oriented. Để làm điều này, ta phải sử dụng hai lớp là BufferedReader và PrintWriter (ta sẽ tìm hiểu sâu hơn các lớp này thông qua các bài viết về Buffered I/O và Formatting).

Lớp CopyLines dưới đây sẽ gọi BufferedReader.readLine và PrintWriter.println nhập liệu và hiển thị mỗi dòng tại một thời điểm.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

CopyLines public class {
  public static void main (String [] args) throws IOException { 

    BufferedReader inputStream = null;
    PrintWriter iutputStream = null; 

    try {
      inputStream = new BufferedReader (new FileReader ( "xanadu.txt"));
      outputStream = new PrintWriter (new FileWriter ( "characteroutput.txt")); 

      String str;
      while ((str = inputStream.readLine ()) != null) {
        outputStream.println(str);
      }
    }
    finally {
      if (inputStream != null) {
        inputStream.close ();
      }
      if (outputStream != null) {
                outputStream .close ();
            }
        }
    }
}

Lời gọi readLine sẽ trả về một dòng văn bản, println sẽ hiển thị kết quả, trong đó nó sẽ đưa thêm ký tự kết thúc dòng tùy thuộc vào hệ điều hành hiện tại, điều này có nghĩa rằng ký tự kết thúc dòng có thể không giống với ký kết thúc dòng trong tập tin đầu vào.

Có rất nhiều cách để cấu trúc văn bản vào và ra ngoài cách sử dụng các ký tự và các dòng, xin xem thêm bài viết Quét và Định dạng.

Buffered Stream

Phần lớn các ví dụ trong các bài viết về I/O tại V1Study là không sử dụng bộ đệm I/O. Điều này có nghĩa là từng thao tác đọc hoặc ghi được xử lý trực tiếp bởi hệ điều hành cơ bản, như vậy thì tính hiệu quả sẽ không cao, vì mỗi yêu cầu input và output thường phải truy cập vào đĩa, network, hoặc một số hoạt động khác, và hệ quả là chi phí sẽ tương đối cao.

Để giảm bớt chi phí, nền tảng Java sử dụng các stream bộ đệm I/O. Các stream bộ đệm input đọc dữ liệu từ một vùng bộ nhớ gọi là một bộ đệm; API input gốc chỉ được gọi khi bộ đệm trống. Tương tự như vậy, các stream bộ đệm output ghi dữ liệu vào bộ đệm, và các API output gốc chỉ được gọi khi bộ đệm đầy.

Một chương trình có thể chuyển đổi một không được đệm vào một stream đệm bằng cách sử dụng các gói sẵn có. Cụ thể, ở lớp CopyCharacters có hai câu lệnh là:

    inputStream = new FileReader ( "xanadu.txt");
    outputStream = new FileWriter ( "characteroutput.txt");

, thì ta có thể sửa lại chúng thành như sau:

inputStream = new BufferedReader (new FileReader ( "xanadu.txt"));

outputStream = new BufferedWriter (new FileWriter ( "characteroutput.txt"));

Có bốn lớp stream đệm dùng để bao các stream không đệm là: BufferedInputStream và BufferedOutputStream tạo byte stream đệm, còn BufferedReader và BufferedWriter tạo character stream đệm.

Xả bộ đệm

Một số lớp output đệm autoflush (xả tự động). Khi kích hoạt xả tự động thì một số sự kiện quan trọng sẽ được kích hoạt để tiến hành xả bộ đệm. Ví dụ, một đối tượng PrintWriter xả tự động sẽ xả bộ đệm mỗi khi gọi phương thức println hoặc format.

Để xả thủ công thì ta gọi phương thức flush. Phương thức flush áp dụng được cho tất cả các output stream, tất nhiên nó chỉ có tác dụng khi stream được đệm.

Scanning and Formatting

Lập trình I/O thường liên quan đến việc dịch thành và dịch từ những dữ liệu được định dạng gần với những gì mà con người thích làm việc. Để giúp ta về những điều này thì nền tảng Java cung cấp hai API là API scanner dùng để nhập liệu với các bit dữ liệu, và API formatting dùng để hiển thị dữ liệu theo định dạng mà con người có thể đọc được.

Quét (Scanning)

Đối tượng của lớp Scanner rất hữu ích cho việc định dạng đầu vào có định dạng vào thẻ và các dịch thẻ cá nhân theo kiểu dữ liệu của họ.

Breaking Input vào Tokens

Theo mặc định, một máy quét sử dụng ký tự dấu cách trắng để phân tách các token (ký tự trắng bao gồm khoảng trống (blank), các tab, và các ký tự kết thúc của dòng). Để xem cách quét hoạt động, chúng ta hãy xem lớp ScanXan sau đây, một chương trình đọc từng từ một từ tập tin xanadu.txt và in chúng ra từng dòng một.

import java.io.*;
import java.util.Scanner; 

public class ScanXan {
    public static void main(String [] args) throws IOException { 

        Scanner s = null; 

        try {
            s = new Scanner(new BufferedReader (new FileReader("xanadu.txt"))); 

            while (s.hasNex ()) {
                System.out.println(s.next ());
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
}

Chú ý rằng ScanXan gọi Scanner 's gần phương thức khi nó được thực hiện với các đối tượng quét. Mặc dù một máy quét không phải là một dòng suối, bạn cần phải đóng lại để chỉ ra rằng bạn đang thực hiện với dòng cơ bản của nó.

Output của lớp ScanXan sẽ như sau:

In
Xanadu
did
Kubla
Khan
A
stately
pleasure-dome
...

Để sử dụng một tách thẻ khác nhau, gọi useDelimiter () , chỉ định một biểu thức chính quy. Ví dụ, giả sử bạn muốn tách token để có một dấu phẩy, tùy theo sau bởi khoảng trắng. Bạn sẽ gọi,

s.useDelimiter ( ",\\s*");

Dịch các token riêng lẻ

Các ScanXan dụ đối xử với tất cả những mã đầu vào đơn giản chuỗi giá trị. Scanner cũng hỗ trợ thẻ cho tất cả các loại nguyên thủy của ngôn ngữ Java (trừ char ), cũng như BigInteger và BigDecimal . Ngoài ra, giá trị số có thể sử dụng hàng ngàn dải phân cách. Như vậy, trong một Mỹ bản địa, Scanner đọc một cách chính xác các chuỗi "32.767" là đại diện cho một giá trị số nguyên.

Chúng tôi có đề cập đến các địa phương, vì hàng ngàn dải phân cách và các ký hiệu thập phân là miền địa phương cụ thể. Vì vậy, ví dụ sau đây sẽ không hoạt động chính xác trong tất cả các miền địa phương nếu chúng ta không xác định rằng các máy quét nên sử dụng Mỹ bản địa. Đó không phải là một cái gì đó bạn thường phải lo lắng về, vì dữ liệu đầu vào của bạn thường xuất phát từ các nguồn mà sử dụng các miền địa phương giống như bạn làm. Nhưng ví dụ này là một phần của Hướng dẫn Java và được phân phối trên toàn thế giới.

Giả sử ta có tập tin "usnumbers.txt" chứa nội dung sau:

8.5
32,767
3.14159
1,000,000.1

Ví dụ ScanSum sau đây đọc các giá trị double từ tập tin "usnumbers.txt" và cộng chúng vào biến sum:

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Scanner;
import java.util.Locale; 

public class ScanSum {
    public static void main(String [] args) throws IOException { 

        Scanner s = null;
        double sum = 0; 

        try {
            s = new Scanner(new BufferedReader (new FileReader ("usnumbers.txt")));
            s.useLocale(Locale.US); 

            while (s.hasNext() ) {
                if (s.hasNextDouble()) {
                    sum + = s.nextDouble();
                } else {
                    s.next ();
                }   
            }
        } finally {
            s.close();
        } 

        System.out.println(sum);
    }
}

Chuỗi đầu ra là "1032778,74159". Thời gian sẽ là một ký tự khác nhau ở một số vùng do System.out là một đối tượng PrintStream, và lớp này không cung cấp một cách để ghi đè lên các miền địa phương mặc định. Ta có thể ghi đè lên các vị trí cho toàn bộ chương trình, hoặc chỉ có thể sử dụng định dạng, như mô tả trong phần tiếp theo sau đây.

Định dạng

Các đối tượng Stream thực hiện việc định dạng là những thể hiện của lớp PrintWriter, là một lớp character stream, hoặc PrintStream, là một lớp byte stream. 


Lưu ý:  Chỉ có các đối tượng của PrintStream là System.out và System.err là bạn cần đến. Khi bạn cần tạo ra một output stream có định dạng thì ta cần PrintWriter, không cần PrintStream.

Giống như tất cả các đối tượng byte stream và character stream, các đối tượng của PrintStream và PrintWriter sẽ thực thi một tập các phương thức write chuẩn cho các byte và ký tự đầu ra đơn giản. Ngoài ra, cả PrintStream và PrintWriter đều thực hiện một tập các phương thức để chuyển đổi dữ liệu nội bộ thành output có định dạng. Có hai mức định dạng như sau:

  • print và println định dạng các giá trị riêng biệt thành các chuẩn khác nhau.
  • format định dạng hầu hết các giá trị dựa trên một chuỗi định dạng, với nhiều tùy chọn để định dạng chính xác.

Các phương thức print và println

Lời gọi đến phương thức print hoặc println sẽ xuất ra một giá trị duy nhất sau khi chuyển đổi các giá trị sử dụng phương thức toString một cách thích hợp. Ta có thể thấy điều này trong ví dụ về lớp Root dưới đây:

public class Root {
    public static void main (String [] args) {
        int i = 2;
        double r = Math.sqrt (i);

        System.out.print("The square root of ");
        System.out.print( i);
        System.out.print( " is ");
        System.out.print(r);
        System.out.println("."); 

        i = 5;
        r = Math.sqrt(i);
        System.out .println("The square root of " + i + " is " + r + ".");
    }
}

Và đây là đầu ra của lớp Root:

The square root of 2 is 1,4142135623730951.
The square root of 5 is 2,23606797749979.

Các biến i và r được định dạng hai lần: lần thứ nhất là sử dụng mã lệnh trong quá tải phương thức print, lần thứ hai là chuyển đổi mã lệnh được sinh ra tự động bởi trình biên dịch Java sử dụng toString. Ta có thể định dạng bất kỳ giá trị nào theo cách này, nhưng không có nhiều quyền kiểm soát kết quả.

Phương thức format

Phương thức này định dạng nhiều đối số dựa trên một chuỗi định dạng. Chuỗi định dạng bao gồm văn bản tĩnh được nhúng với các bộ đặc tả định dạng; nếu không có các bộ đặc tả định dạng thì chuỗi là đầu ra sẽ không thay đổi.

Chuỗi định dạng hỗ trợ nhiều tính năng, bài viết này chỉ giới thiệu một số tính năng cơ bản, xin xem thêm tại ĐÂY.

Ví dụ Root2 sau đây định dạng hai giá trị có kiểu khác nhau trong một lời gọi đến hàm format:

public class Root2 {
    public static void main(String [] args) {
        int i = 2;
        double r = Math.sqrt(i); 

        System.out.format("Căn bậc hai của %d là %f%n." , i, r);
    }
}

Dưới đây là kết quả:

Căn bậc hai của 2 là 1,414214.

Tất cả các bộ đặc tả định dạng đều bắt đầu bằng ký tự % và kết thúc bằng một 1 hoặc 2 ký tự chuyển đổi để xác định các loại định dạng đầu ra được tạo ra. Ví dụ trên có 3 định dạng được sử dụng là

  • d định dạng một giá trị số nguyên.
  • e định dạng một giá trị dấu chấm động.
  • n xuất ra một ký tự xuống dòng.

Dưới đây là một số dạng chuyển đổi khác:

  • x định dạng một số nguyên như là một giá trị dạng thập lục phân (hệ 16).
  • s định dạng cho một chuỗi.
  • tB định dạng một số nguyên như là một tên tháng địa phương cụ thể.

Có nhiều chuyển đổi khác.


Chú ý: Ngoại trừ %% và %n, tất cả các bộ đặc tả định dạng phải phù hợp với một đối số, nếu không thì một ngoại lệ sẽ được ném ra.

Trong ngôn ngữ lập trình Java, \n luôn tạo ra ký tự line-feed (\u000A ). \n chỉ không được sử dụng đến trừ khi bạn muốn có một ký tự line-feed. Để có được những dòng phân cách chính xác cho các nền tảng cục bộ thì ta sử dụng %n .


Ngoài việc chuyển đổi, một bộ đặc tả định dạng có thể chứa một số thành phần bổ sung để tùy chỉnh đầu ra có định dạng. Dưới đây là một ví dụ:

public class Format{
    public static void main (String [] args) {
        System.out.format( "%f, %1$+020.10f %n", Math.PI);
    }
}

Dưới đây là kết quả:

3.141593, +00000003.1415926536

Hình dưới đây mô tả cho định dạng ở ví dụ trên:

Các yếu tố của một đặc tả định dạng
Các thành phần của một Bộ đặc tả định dạng.

Các thành phần này phải xuất hiện theo thứ tự hiển thị. Các thành phần từ phải sang trái là:

- Precision : Đối với các giá trị dấu chấm động, đây là độ chính xác toán học của các giá trị được định dạng. Đối với định dạng s (dành cho chuỗi) thì đây là độ rộng tối đa của giá trị được định dạng; giá trị sẽ được cắt bớt nếu cần thiết.

- Width : Độ rộng tối thiểu của giá trị được định dạng; giá trị được đệm nếu cần thiết. Theo mặc định, giá trị được đệm thêm các dấu cách vào bên trái.

- Flags : Xác định các tùy chọn định dạng bổ sung. Trong ví dụ về lớp Format ở trên thì cờ + xác định rằng số lượng luôn phải được định dạng với một dấu hiệu, và cờ 0 xác định rằng 0 là ký tự đệm thêm. Các cờ khác bao gồm dấu trừ - (đệm sang bên phải) và dấu phẩy , (định dạng cho số để phân cách các hàng phần nghìn). Lưu ý rằng một số cờ không thể được sử dụng với một số cờ khác hoặc với các chuyển đổi nhất định.

- Argument Index cho phép tương thích một cách rõ ràng một đối số được chỉ định. Bạn cũng có thể sử dụng < để tương thích với một số đối số giống như bộ đặc tả trước. Như vậy, ví dụ về định dạng ở trên có thể viết lại thành: System.out.format("%f, %<+020.10f %n", Math.PI);

I/O từ Command Line

Một chương trình thường được chạy từ dòng lệnh và tương tác với người sử dụng trong môi trường dòng lệnh. Các nền tảng Java hỗ trợ loại tương tác này theo hai cách: thông qua các stream chuẩn và thông qua Cosole.

Các stream chuẩn

Chuẩn Stream là một tính năng của nhiều hệ điều hành. Theo mặc định, chúng đọc vào dữ liệu từ bàn phím và ghi dữ liệu ra màn hình. Chúng cũng hỗ trợ I/O trên các tập tin và giữa các chương trình, nhưng tính năng đó được điều khiển bởi trình thông dịch dòng lệnh mà không phải bởi chương trình.

Các nền tảng Java hỗ trợ ba stream chuẩn: Standard Input, được truy cập thông qua System.inStandard Output, được truy cập thông qua System.out; và Standard Error, được truy cập thông qua System.err. Những đối tượng sẽ được xác định tự động và không cần phải được mở. Các Standard Output và Standard Error đều là những output, có đầu ra lỗi riêng biệt cho phép người dùng chuyển hướng đầu ra thường xuyên vào một tập tin và vẫn có thể đọc được các thông báo lỗi.

Bạn cũng có thể sử dụng các stream chuẩn như là các character stream, nhưng vì lý do lịch sử, cách stream chuẩn lại là những byte stream. System.out và System.err được định nghĩa là những đối tượng PrintStream. Mặc dù là kỹ thuật byte stream, nhưng PrintStream sử dụng một đối tượng character stream nội bộ để mô phỏng các tính năng của các character stream.

Ngược lại, System.in là một byte stream và không có tính năng của character stream. Để sử dụng đầu vào tiêu chuẩn như là một Standard Input thì ta bao System.in trong InputStreamReader như sau:

InputStreamReader cin = new InputStreamReader (System.in);

Console

Một lựa chọn tiên tiến hơn cho các stream chuẩn là Console, đây là một đối tượng đơn và được định sẵn có kiểu Console với hầu hết các tính năng được cung cấp bởi các stream chuẩn bên cạnh những thành phần khác. Console đặc biệt hữu ích cho các mục nhập mật khẩu an toàn. Các đối tượng Console cũng cung cấp các input và output stream cho các character stream thông qua các phương thức readerwriter.

Trước khi một chương trình có thể sử dụng Console, nó phải cố gắng để lấy đối tượng Console bằng cách gọi System.Console(). Nếu đối tượng Console có sẵn thì phương thức này trả về chính nó. Nếu System.Console trả về NULL thì các hoạt động của Console không được phép, có thể hệ điều hành không hỗ trợ hoặc vì chương trình đã được chạy trong một môi trường không tương tác.

Các đối tượng Console hỗ trợ nhập mật khẩu an toàn thông qua phương thức readPassword. Phương thức này giúp nhập mật khẩu an toàn trong hai cách, cách thứ nhất là nó ngăn chặn echoing, vì thế mật khẩu sẽ không hiển thị trên màn hình của người dùng, cách thứ hai là readPassword trả về một mảng ký tự, không phải là một chuỗi, do đó mật khẩu có thể được ghi đè, loại bỏ nó khỏi bộ nhớ ngay khi nó không còn cần thiết.

Dưới đây là ví dụ Password là chương trình nguyên mẫu để thay đổi mật khẩu của người dùng. Nó cũng cho thấy cách sử dụng của một số phương thức Console.

import java.io.Console;
import java.util.Arrays;
import java.io.IOException;

public class Password { 

    public static void main(String args []) throws IOException { 

        Console c = System.Console();
        if(c == null) {
            System.err.println("Không có giao diện điều khiển.");
            System.exit(1);
        } 

        String login = c.readLine( "Nhập thông tin đăng nhập: ");
        char [] oldPassword = c.readPassword( "Nhập mật khẩu cũ của bạn: "); 

        if (veryfy(login, oldPassword)) {
            noMatch boolean;
            do {
                char [] newPassword1 = c.readPassword("Nhập mật khẩu mới của bạn: ");
                char [] newPassword2 = c.readPassword("Nhập mật khẩu mới một lần nữa: ");
                noMatch =! Arrays.equals (newPassword1, newPassword2);
                if (noMatch) {
                    c.format( "Mật khẩu không phù hợp. Hãy thử lại.%n");
                } Else {
                    change(login, newPassword1);
                    c.format("Mật khẩu của %s đã thay đổi.%n", login);
                }
                Arrays.fill(newPassword1, '');
                Arrays.fill(newPassword2, '');
            } while(noMatch);
        } 

        Arrays.fill(oldPassword, '');
    } 

    //Tùy ý thay đổi phương pháp.
    boolean veryfy(String login, char[] password) {
        // Phương thức này luôn luôn trả về
        // true trong ví dụ này.
        // Sửa đổi phương pháp này để xác minh
        // mật khẩu theo quy tắc của bạn.
        return true;
    } 

    //Tùy ý thay đổi phương thức.
    static void change(String login, char[] password) {
        // Sửa đổi phương thức này để thay đổi
        // mật khẩu theo quy tắc của bạn.
    }
}

Lớp Password thực thi theo các bước sau:

  1. Cố gắng để lấy đối tượng Console. Nếu đối tượng không có sẵn thì hủy bỏ.

  2. Gọi Console.ReadLine để nhắc và đọc tên đăng nhập của người dùng.

  3. Gọi Console.readPassword để nhắc và đọc mật khẩu hiện tại của người dùng.

  4. Gọi phương thức veryfy để xác nhận rằng người dùng được phép thay đổi mật khẩu (trong ví dụ này, veryfy là một phương thức giả định luôn luôn trả về true).

  5. Lặp lại các bước sau cho đến khi người dùng nhập mật khẩu giống nhau hai lần:

    a. Gọi Console.readPassword hai lần để nhắc nhở cho và đọc một mật khẩu mới.

    b. Nếu người dùng nhập mật khẩu giống nhau cả hai lần, gọi phương thức change để thay đổi nó (change cũng là phương thức tưởng.)

  6. Ghi đè lên cả hai mật khẩu bằng các khoảng trống.

  7. Ghi đè lên các mật khẩu cũ bằng các khoảng trống.

Data Stream

Data stream hỗ trợ các hoạt động I/O nhị phân đối với các giá trị có kiểu dữ liệu nguyên thủy và các giá trị kiểu chuỗi (String). Tất cả các data stream đều thực thi một trong hai giao diện là DataInput hoặc DataOutput. Bài viết này tập trung vào việc thực thi các giao diện DataInputStream và DataOutputStream.

Ví dụ có tên DataStreams dưới đây trình bày data stream bằng cách ghi ra một tập hợp các bản ghi dữ liệu, và sau đó đọc chúng.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.EOFException;
 
public class DataStreams {
    static final String dataFile = "invoicedata";
 
    static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
    static final int[] units = { 12, 8, 13, 29, 50 };
    static final String[] descs = { "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain" };
 
    public static void main(String[] args) throws IOException {
 
  
        DataOutputStream out = null;
         
        try {
            out = new DataOutputStream(new
                    BufferedOutputStream(new FileOutputStream(dataFile)));
 
            for (int i = 0; i < prices.length; i ++) {
                out.writeDouble(prices[i]);
                out.writeInt(units[i]);
                out.writeUTF(descs[i]);
            }
        } finally {
            out.close();
        }
 
        DataInputStream in = null;
        double total = 0.0;
        try {
            in = new DataInputStream(new
                    BufferedInputStream(new FileInputStream(dataFile)));
 
            double price;
            int unit;
            String desc;
 
            try {
                while (true) {
                    price = in.readDouble();
                    unit = in.readInt();
                    desc = in.readUTF();
                    System.out.format("You ordered %d units of %s at $%.2f%n",
                            unit, desc, price);
                    total += unit * price;
                }
            } catch (EOFException e) { }
            System.out.format("For a TOTAL of: $%.2f%n", total);
        }
        finally {
            in.close();
        }
    }
}

Mỗi bản ghi bao gồm ba giá trị liên quan đến một sản phẩm trên một hóa đơn, như thể hiện trong bảng sau:

Thứ tự Loại dữ liệu Mô tả dữ liệu Phương thức output Phương thức input Giá trị mẫu
1 double Giá sản phẩm DataOutputStream.writeDouble DataInputStream.readDouble 19.99
2 int đơn vị tính DataOutputStream.writeInt DataInputStream.readInt 12
3 String Mô tả sản phẩm DataOutputStream.writeUTF DataInputStream.readUTF "Java T-Shirt"

Hãy kiểm tra mã số rất quan trọng trong DataStreams. Đầu tiên, chương trình định nghĩa một số hằng số chứa tên của các tập tin dữ liệu và dữ liệu sẽ được ghi vào nó:

static final String datafile = "invoicedata"; 

static final double [] prices = {19,99, 9.99, 15.99, 3.99, 4.99};
static final int [] units = {12, 8, 13, 29, 50};
static final String[] descs = {
    "Java T-shirt",
    "Java Mug",
    "Duke Juggling Dolls",
    "Java Pin",
    "Java Keychain"
};

Sau đó DataStreams mở một output stream. Vì mỗi DataOutputStream chỉ có thể được tạo ra như là một wrapper cho một đối tượng byte stream hiện có, nên DataStreams sẽ cung cấp một tập tin được đệm để output byte stream.

out = new DataOutputStream(new BufferedOutputStream(
              new FileOutputStream(datafile)));

//DataStreams ghi ra các bản ghi và đóng output stream.

for (int i = 0; i <prices.length; i++) {
    out.writeDouble(prices[i]);
    out.writeInt(units[i]);
    out.writeUTF(descs[i]);
}

Phương thức writeUTF sẽ ghi ra các giá trị String theo định dạng UTF-8 đã sửa đổi, đây là một mã ký tự chiều rộng thay đổi mà chỉ cần một byte duy nhất cho các ký tự phương Tây thông thường.

Bây giờ DataStreams đọc dữ liệu trở lại. Đầu tiên nó phải cung cấp một input stream và các biến để lưu dữ liệu đầu vào. Giống như DataOutputStreamDataInputStream phải được xây dựng như là một wrapper cho một byte stream.

in = new DataInputStream(new BufferedInputStream(new FileInputStream (datafile))); 

double price;
int unit;
String desc;
double total = 0.0;

Bây giờ DataStreams có thể đọc mỗi bản ghi trong stream, báo cáo về dữ liệu mà nó gặp.

try {
    while (true) {
        price = in.readDouble();
        unit = in.readInt();
        desc = in.readUTF();
        System.out.format("Bạn đã đặt %d" + "sản phẩm %s với giá $%.2f%n", unit, desc, price);
        total += unit * price;
    }
} catch(EOFException e) {
}

Chú ý rằng DataStreams phát hiện một điều kiện cuối tập tin bằng cách bắt ngoại lệ EOFException thay vì kiểm tra một giá trị trả về không hợp lệ. Tất cả các thực thi của các phương thức của DataInput đều sử dụng EOFException thay vì các giá trị trả về.

Cũng lưu ý rằng mỗi phương thức write cụ thể trong DataStreams được kết hợp chính xác với một phương thức read tương ứng. Vậy nên, lập trình viên cần đảm bảo rằng các kiểu output và input phải tương thích với nhau theo cách thức sau: Các input stream bao gồm các dữ liệu nhị phân đơn giản, không cần chỉ ra các loại giá trị riêng, hoặc vị trí bắt đầu của chúng trong stream.

Ở đây, DataStreams sử dụng một kỹ thuật lập trình rất tồi là: nó sử dụng các số dấu chấm động (số thực) để thể hiện các giá trị tiền tệ, mà kiểu dấu chấm động không thích hợp cho giá trị chính xác. Nó đặc biệt tồi khi áp dụng cho các phân số thập phân, bởi vì những ước số chung (như là 0.1 chẳng hạn) không thể hiện được ở dạng nhị phân.

Các kiểu chuẩn áp dụng cho các giá trị tiền tệ là java.math.BigDecimal. Tuy nhiên, BigDecimal là một kiểu đối tượng, vì vậy nó sẽ không làm việc với data stream.Tuy nhiên, BigDecimal sẽ làm việc với object stream, được trình bày ở bài viết tiếp theo.

Object Stream

Data stream hỗ trợ I/O đối với các kiểu dữ liệu nguyên thủy, còn object stream hỗ trợ I/O đối với các đối tượng. Hầu hết các lớp chuẩn hỗ trợ tính tuần tự (serialization) đối với các đối tượng của chúng. Những lớp này sẽ thực thi giao diện Serializable.

Có hai lớp object stream bao gồm ObjectInputStream và ObjectOutputStream, những lớp này thực thi ObjectInput và ObjectOutput, đây là những giao diện con của DataInput và DataOutput. Có nghĩa rằng tất cả các phương thức I/O đối với các dữ liệu kiểu nguyên thủy được bảo đảm với data stream thì cũng được thực thi với object stream. Vì vậy, mỗi object stream có thể chứa một hỗn hợp của các giá trị nguyên thủy và đối tượng. Ví dụ có tên ObjectStreams dưới đây sẽ minh họa điều này:

import java.io.*;
import java.math.BigDecimal;
import java.util.Calendar;
 
public class ObjectStreams {
    static final String dataFile = "invoicedata";
 
    static final BigDecimal[] prices = {
        new BigDecimal("19.99"),
        new BigDecimal("9.99"),
        new BigDecimal("15.99"),
        new BigDecimal("3.99"),
        new BigDecimal("4.99") };
    static final int[] units = { 12, 8, 13, 29, 50 };
    static final String[] descs = { "Java T-shirt",
            "Java Mug",
            "Duke Juggling Dolls",
            "Java Pin",
            "Java Key Chain" };
 
    public static void main(String[] args)
        throws IOException, ClassNotFoundException {
 
  
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new
                    BufferedOutputStream(new FileOutputStream(dataFile)));
 
            out.writeObject(Calendar.getInstance());
            for (int i = 0; i < prices.length; i ++) {
                out.writeObject(prices[i]);
                out.writeInt(units[i]);
                out.writeUTF(descs[i]);
            }
        } finally {
            out.close();
        }
 
        ObjectInputStream in = null;
        try {
            in = new ObjectInputStream(new
                    BufferedInputStream(new FileInputStream(dataFile)));
 
            Calendar date = null;
            BigDecimal price;
            int unit;
            String desc;
            BigDecimal total = new BigDecimal(0);
 
            date = (Calendar) in.readObject();
 
            System.out.format ("On %tA, %<tB %<te, %<tY:%n", date);
 
            try {
                while (true) {
                    price = (BigDecimal) in.readObject();
                    unit = in.readInt();
                    desc = in.readUTF();
                    System.out.format("You ordered %d units of %s at $%.2f%n",
                            unit, desc, price);
                    total = total.add(price.multiply(new BigDecimal(unit)));
                }
            } catch (EOFException e) {}
            System.out.format("For a TOTAL of: $%.2f%n", total);
        } finally {
            in.close();
        }
    }
}

Lớp ObjectStreams tạo các ứng dụng tương tự như lớp DataStreams, với hai thay đổi, thứ nhất, giá lúc này là các đối tượng thuộc lớp BigDecimal, để thể hiện tốt hơn các giá trị phân số, thứ hai, một đối tượng Calendar được ghi vào tập tin dữ liệu, nó thể hiện ngày lập hóa đơn.

Nếu phương thức readObject() không trả về kiểu đối tượng theo dự kiến thì ta cố gắng ép sang kiểu đúng để có thể ném một ngoại lệ ClassNotFoundException. Trong ví dụ đơn giản này thì điều đó không thể xảy ra, vì vậy ta không cần cố bắt các ngoại lệ, thay vào đó, ta thông báo cho trình biên dịch rằng ta nhận thức được vấn đề này bằng cách thêm ngoại lệ ClassNotFoundException vào mệnh đề throws của phương thức main().

Đầu ra và đầu vào của đối tượng Complex

Các phương thức writeObject và readObject rất dễ dùng, nhưng chúng chứa một số logic quản lý đối tượng rất tinh vi. Điều này không quan trọng đối với một lớp như lớp Calendar, nó chỉ quan trọng đối với các giá trị kiểu nguyên thủy. Tuy nhiên, có nhiều đối tượng chứa tham chiếu đến các đối tượng khác. Nếu readObject dùng để tạo lại đối tượng từ stream, thì nó có thể để tạo lại tất cả các đối tượng mà đối tượng gốc đã tham chiếu tới. Vậy nên, những đối tượng bổ sung này có thể có tham khảo riêng của chúng. Trong tình huống này, writeObject sẽ đi qua toàn bộ các tham chiếu đối tượng và ghi chúng lên stream. Như vậy thì, một lời gọi duy nhất của writeObject có thể gây ra một số lượng lớn các đối tượng được ghi vào stream.

Điều này được thể hiện trong hình sau đây, trong đó phương thức writeObject được gọi để viết một đối tượng duy nhất được đặt tên là a. Đối tượng này chứa các tham chiếu đến các đối tượng b và c, trong khi b chứa tham chiếu đến d và e. Lời gọi phương thức writeObject(a) không chỉ ghi a, mà còn là tất cả các đối tượng cần thiết để tái tạo a, vì vậy bốn đối tượng khác trong hình cũng được ghi. Khi a được đọc lại bởi phương thức readObject thì bốn đối tượng khác cũng được đọc lại, và tất cả các đối tượng tham chiếu gốc được bảo toàn.

I / O của nhiều gọi đến đối tượng
I/O của nhiều gọi đến đối tượng

Có một câu hỏi đặt ra là điều gì sẽ xảy ra nếu hai đối tượng trên cùng một stream đều chứa các tham chiếu đến cùng một đối tượng. Chúng có được tham chiếu tới một đối tượng duy nhất khi chúng được đọc lại không? Câu trả lời là "có". Mỗi stream chỉ có thể chứa một bản sao của một đối tượng, mặc dù nó có thể chứa bất kỳ số tham chiếu nào đến nó. Vì vậy, nếu bạn ghi một cách tường minh một đối tượng đến một stream hai lần thì bạn đang thực sự chỉ ghi tham chiếu hai lần. Ví dụ, nếu đoạn mã sau đây viết một đối tượng có tên ob hai lần cho một stream:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

Mỗi phương thức writeObject phải được kết hợp với một phương thức readObject, vì vậy mà đoạn mã đọc stream trở lại sẽ giống như thế này:

Object ob1 = in.readObject();
đối tượng ob2 = in.readObject();

Điều này dẫn đến hai biến ob1 và ob2 tham chiếu đến một đối tượng duy nhất.

Tuy nhiên, nếu một đối tượng duy nhất được ghi tới hai dòng khác nhau thì hiệu quả sẽ được nhân đôi - một chương trình duy nhất đọc cả hai stream trở lại sẽ thấy hai đối tượng riêng biệt.

Đường dẫn là gì? (và sự kiện hệ thống tập tin khác)

Một hệ thống tập tin lưu trữ và tổ chức các tập tin trên một số hình thức của phương tiện truyền thông, nói chung là một hoặc nhiều ổ đĩa cứng, theo một cách mà chúng có thể dễ dàng truy xuất. Hầu hết các hệ thống tập tin sử dụng ngày nay lưu trữ các tập tin theo cấu trúc dạng cây (hoặc phân cấp). Ở phía trên cùng của cây là một (hoặc nhiều) nút gốc. Dưới nút gốc có những tập tin và thư mục (giống như các thư mục trong Microsoft Windows). Mỗi thư mục có thể chứa các tập tin và thư mục con, rồi mỗi thư mục con này lại có thể chứa các tập tin và thư mục con khác, ..., nói chung là có khả năng đến độ sâu gần như vô hạn.

Đường dẫn là gì?

Hình dưới đây cho thấy một cây thư mục mẫu chứa một nút gốc duy nhất. Microsoft Windows hỗ trợ nhiều nút gốc. Mỗi nút gốc ánh xạ đến một vùng nhớ, chẳng hạn như C:\ hoặc D:\. Hệ điều hành Solaris hỗ trợ một nút gốc duy nhất, biểu hiện bằng ký tự dấu gạch chéo, '/'.

cấu trúc thư mục Sample
Ví dụ về cấu trúc thư mục

Một tập tin được xác định bằng đường dẫn của nó thông qua các hệ thống tập tin, bắt đầu từ nút gốc. Ví dụ, tập tin statusReport trong hình trên được mô tả như sau trong hệ điều hành Solaris:

/Home/sally/statusReport

Trong Microsoft Windows, statusReport được mô tả như sau:

C:\home\sally\statusReport

Ký tự được sử dụng để tách các tên thư mục (cũng được gọi là dấu phân cách) là xác định cho các hệ thống tập tin như sau: Hệ điều hành Solaris sử dụng dấu gạch chéo (/), và Microsoft Windows sử dụng các dấu gạch chéo dấu chéo ngược (\).

Tương đối hay tuyệt đối

Mỗi đường dẫn có thể là tương đối hoặc tuyệt đối. Một đường dẫn tuyệt đối luôn chứa nút gốc và danh sách thư mục hoàn chỉnh cần thiết để xác định vị trí các tập tin. Ví dụ, /home/sally/statusReport là một đường dẫn tuyệt đối. Tất cả các thông tin cần thiết để xác định vị trí các tập tin được chứa trong chuỗi đường dẫn.

Một đường dẫn tương đối cần phải được kết hợp với đường dẫn khác để truy cập vào một tập tin. Ví dụ, joe/foo là một đường dẫn tương đối. Nếu không có thêm thông tin, chương trình không thể xác định chính xác vị trí thư mục joe/foo trong hệ thống tập tin.

Liên kết symbolic

Các đối tượng hệ thống tập tin phổ biến nhất là các thư mục và tập tin, và ta đã quen thuộc đối với các đối tượng này. Nhưng một số hệ thống tập tin cũng bao gồm các liên kết tượng trưng. Một liên kết tượng trưng cũng được gọi symlink hoặc soft link (liên kết mềm).

Liên kết tượng trưng là một tập tin đặc biệt được dùng như là một tham chiếu đến tập tin khác. Đối với hầu hết các phần, liên kết tượng trưng là trong suốt trong các ứng dụng, và các hoạt động trên các liên kết tượng trưng được tự động chuyển đến đích của liên kết. (Các tập tin hoặc thư mục được chỉ để được gọi là đích của liên kết). Ngoại lệ sẽ xảy ra khi một liên kết tượng trưng được xóa hoặc đổi tên trong trường hợp bản thân liên kết bị xóa hoặc đổi tên và không phải là mục tiêu của liên kết.

Trong hình dưới đây, logfile dường như là một tập tin mà người dùng nhìn thấy, nhưng nó thực sự là một liên kết đến thư mục /logs/HomeLogFile. HomeLogFile là đích của liên kết.

liên kết tượng trưng mẫu
Ví dụ về một liên kết tượng trưng.

Liên kết tượng trưng thường là trong suốt đối với người sử dụng. Việc đọc hoặc ghi cho một liên kết tượng trưng là giống như việc đọc hoặc ghi cho bất kỳ tập tin hoặc thư mục nào khác.

Cụm từ giải quyết một liên kết có nghĩa là để thay thế vị trí thực sự trong hệ thống tập tin cho các liên kết tượng trưng. Trong ví dụ, việc giải quyết logfile sẽ sinh ra dir/logs/HomeLogFile.

Trong kịch bản thực tế, hầu hết các hệ thống tập tin sử dụng tự do các liên kết tượng trưng. Đôi khi, một liên kết tượng trưng được tạo ra ẩu có thể gây ra một tham chiếu vòng tròn. Một tham chiếu vòng tròn xảy ra khi mục tiêu của một liên kết lại trỏ vào liên kết ban đầu. Các tham chiếu vòng tròn có thể là gián tiếp: thư mục a trỏ tới thư mục b, b trỏ vào thư mục c, trong c có một thư mục con trỏ đến thư mục a. Các tham chiếu vòng tròn có thể gây ra tàn phá khi một chương trình đệ quy đi bộ qua một cấu trúc thư mục. Tuy nhiên, kịch bản này đã được tính toán và sẽ không gây ra việc lặp vô hạn cho chương trình của bạn.

Lớp Path

Lớp Path được giới thiệu từ phiên bản Java SE 7, là một trong những điểm đầu vào chính của gói java.nio.file. Nếu ứng dụng của bạn sử dụng tập tin I/O thì bạn sẽ cần tìm hiểu về những tính năng mạnh mẽ của lớp này.


Lưu ý phiên bản:  Nếu bạn đã có mã lệnh ứng với phiên bản trước JDK7 và muốn sử dụng java.io.File, thì bạn vẫn có thể tận dụng lợi thế của lớp Path bằng cách sử dụng phương thức File.toPath.

Như tên gọi của nó, lớp Path là một đại diện chương trình của một đường dẫn trong hệ thống tập tin. Mỗi đối tượng Path có chứa danh sách tên tập tin và thư mục dùng để xây dựng đường dẫn, và được sử dụng để kiểm tra, xác định vị trí và thao tác tập tin.

Đối tượng của lớp Path phản ánh nền tảng cơ bản. Trong hệ điều hành Solaris thì Path sử dụng cú pháp Solaris (ví dụ: /home/joe/foo) và trong Microsoft Windows thì nó sử dụng cú pháp Windows (ví dụ như C:\home\joe\foo). Path không phải là hệ thống độc lập. Ta không thể so sánh Path ở hệ thống tập tin Solaris và mong muốn nó để phù hợp với Path ở hệ thống tập tin Windows, ngay cả khi các cấu trúc thư mục là giống nhau và cả hai trường hợp xác định vị trí các tập tin tương đối giống nhau.

Các tập tin hoặc thư mục tương ứng với Path có thể không tồn tại. Bạn có thể tạo ra một đối tượng Path và thao tác với nó theo nhiều cách khác nhau: bạn có thể gắn thêm phần đường dẫn vào nó, trích ra đường dẫn từ nó, so sánh nó với đường dẫn khác. Tại thời điểm thích hợp, ta có thể sử dụng các phương thức trong lớp File để kiểm tra sự tồn tại của tập tin tương ứng với đường dẫn, tạo ra các tập tin, mở nó, xóa, thay đổi quyền hạn của nó.

Lớp Path có nhiều phương thức khác nhau có thể được sử dụng để có được thông tin về đường dẫn, các thành phần truy cập của đường dẫn, chuyển đổi đường dẫn sang các dạng khác, hoặc trích xuất một phần đường dẫn. Ngoài ra còn có phương thức phù hợp với các chuỗi đường dẫn và phương thức để loại bỏ phần thừa của đường dẫn. Bài học này đề cập đến những phương thức của lớp Path, đôi khi được gọi là các hoạt động cú pháp, bởi vì chúng hoạt động trên đường dẫn riêng và không truy cập vào hệ thống tập tin.

Phần này bao gồm những điều sau đây:

  • Tạo một đường dẫn
  • Lấy thông tin về đường dẫn
  • Loại bỏ dư thừa khỏi đường dẫn
  • Chuyển đổi đường dẫn
  • Nối hai đường dẫn
  • Tạo một đường dẫn từ hai đường dẫn
  • So sánh hai đường dẫn

Tạo Path

Một đường dẫn có chứa các thông tin được sử dụng để xác định vị trí của một tập tin hoặc thư mục. Tại thời điểm được xác định, một đường dẫn được cung cấp với một loạt của một hoặc nhiều tên. Một phần tử gốc hoặc tên một tập tin có thể được bao gồm, nhưng không phải là bắt buộc. Một đường dẫn có thể bao gồm chỉ một thư mục hoặc tên tập tin duy nhất.

Bạn có thể dễ dàng tạo ra một đối tượng đường dẫn bằng cách sử dụng một trong các phương thức get sau đây từ lớp trợ giúp Paths:

Path p1 = Paths.get("/tmp/foo");
Path p2 = Paths.get(args[0]);
Path p3 = Paths.get(URI.create("file:///Users/joe/FileTest.java"));

Phương thức Paths.get là cách viết tắt của đoạn mã sau đây:

Path p4 = FileSystems.getDefault().getPath("/users/sally");

Ví dụ sau tạo đường dẫn /u/joe/logs/foo.log với giả sử rằng thư mục home của ta là /u/joe, hoặc C:\joe\logs\foo.log nếu bạn đang sử dụng hệ điều hành Windows.

Path p5 = Paths.get(System.getProperty( "user.home"),"logs", "foo.log");

Truy xuất thông tin về đường dẫn

Bạn có thể nghĩ đến đường dẫn giống như việc lưu trữ tên của những phần tử thành một chuỗi. Các phần tử cao nhất trong cấu trúc thư mục sẽ được đặt chỉ số 0, các phần tử thấp nhất trong cấu trúc thư mục sẽ được đặt chỉ số [n-1], với n là số lượng tên các phần tử trong đường dẫn. Các phương thức có sẵn dùng để truy xuất các phần tử riêng biệt hoặc một chuỗi con các đường dẫn sử dụng các chỉ số.

Các ví dụ trong bài viết này sử dụng cấu trúc thư mục sau:

cấu trúc thư mục Sample
Cấu trúc thư mục mẫu

Đoạn mã sau định nghĩa một đối tượng Path và sau đó gọi một số phương thức để có được thông tin về đường dẫn:

// Không một phương thức nào trong số những phương thức này yêu cầu rằng các tập tin tương ứng với Path phải tồn tại.
// Cú pháp Microsoft Windows
Path path = Paths.get("C:\\home\\joe\\foo");

// Cú pháp Solaris
Path path = Paths.get( "/home/joe/foo"); 

System.out.format("toString: %s%n", path.toString());
System.out.format("getFileName: %s%n", path.getFileName());
System.out.format("getName(0): %s%n", path.getName(0));
System.out.format("getNameCount: %d%n", path.getNameCount());
System.out.format("subpath(0,2): %s%n", path.subpath(0,2));
System.out.format("getParent: %s%n", path.getParent());
System.out.format("getRoot: %s%n", path.getRoot());

Đây là output cho cả hệ điều hành Windows và Solaris:

Phương thức được gọi Solaris Microsoft Windows Chú thích
toString /home/joe/foo C:\home\joe\foo Trả về chuỗi thể hiện đường dẫn. Nếu đường dẫn đã được tạo ra sử dụng Filesystems.getDefault().GetPath(String) hoặc Paths.get(sau này là phương thức thuận tiện cho getPath), thì phương thức thực hiện việc xóa cú pháp nhỏ. Ví dụ, trong một hệ điều hành UNIX, nó sẽ sửa chuỗi đầu vào //home/joe/foo thành /home/joe/foo.
getFileName foo foo Trả về tên tập tin hoặc thành phần cuối cùng của chuỗi tên các thành phần.
getName (0) home home Trả về thành phần đường dẫn tương ứng với chỉ số xác định. Thành phần chỉ số 0 là thành phần gần gốc nhất.
getNameCount 3 3 Trả về số lượng các thành phần trong đường dẫn.
subpath(0,2) home/joe home\joe Trả về dãy con của các đường dẫn (không bao gồm thành phần gốc) theo quy định của các chỉ số đầu và chỉ số kết thúc.
getParent /home/joe \home\joe Trả về đường dẫn của thư mục cha.
getRoot / C:\ Trả về gốc của đường dẫn.


Ví dụ trên cho thấy đầu ra cho một đường dẫn tuyệt đối. Trong ví dụ sau đây, một đường dẫn tương đối được xác định:

// Cú pháp Solaris
Path path = Paths.get("sally/bar");
//hoặc
// Cú pháp Microsoft Windows
Path path = Paths.get("sally\\bar");

Còn dây là đầu ra dành cho Windows và Solaris:

Phương thức được gọi Solaris Returns trong Microsoft Windows
toString sally/bar sally\bar
getFileName bar bar
getName(0) sally sally
getNameCount 2 2
subpath(0,1) sally sally
getParent sally sally
getRoot null null

Loại bỏ dư thừa từ đường dẫn

Nhiều hệ thống tập tin sử dụng ký hiệu "." để biểu thị thư mục hiện thời và ".." để biểu thị các thư mục cha. Có thể có tình huống trong đó đường dẫn chứa thông tin thư mục dự phòng. Có lẽ một máy chủ được cấu hình để lưu file log của nó trong thư mục "/dir/logs/.", và ta muốn xóa các dấu ký hiệu "/." từ đường dẫn.

Hai ví dụ dưới đây cho thấy sự dư thừa:

/home/./joe/foo
/home/sally/../joe/foo

Phương thức normalize  loại bỏ bất kỳ thành phần dư thừa nào, trong đó bao gồm cả "." và "directory/..". Cả hai ví dụ trên đều sẽ được chuyển thành dạng thông thường là /home/joe/foo.

Có điều quan trọng cần lưu ý là phương thức normalize không kiểm tra tại các hệ thống tập tin khi nó làm sạch một đường dẫn. Đây là hoạt động hoàn toàn tuân theo cú pháp. Trong ví dụ thứ hai, nếu sally là một liên kết tượng trưng, thì việc ​​loại bỏ sally/.. có thể dẫn đến là không còn định vị đúng đường dẫn tới các tập tin mong muốn.

Để làm sạch một đường dẫn trong khi đảm bảo rằng kết quả của việc định vị các tập tin là chính xác, ta có thể sử dụng phương thức toRealPath. Phương thức này được mô tả trong các phần tiếp sau đây.

Chuyển đổi đường dẫn

Bạn có thể sử dụng ba phương thức để chuyển đổi các đường dẫn . Nếu bạn cần phải chuyển đổi đường dẫn thành một chuỗi có thể được mở ra từ một trình duyệt, bạn có thể sử dụng toUri. Ví dụ:

Path p1 = Paths.get("/home/logfile");
//Kết quả là file: ///home/logfile
System.out.format( "% s%n", p1.toUri());

Các phương thức toAbsolutePath chuyển đổi một đường dẫn thành một đường dẫn tuyệt đối, nếu thành công thì nó trả về cùng một đối tượng Path. phương thức toAbsolutePath có thể rất hữu ích khi xử lý tên tập tin người dùng nhập vào. Ví dụ:

public class FileTest {
    public static void main(String [] args) { 

        if (args.length <1) {
            System.out.println("sử dụng: tập tin FileTest");
            System.exit(-1);
        } 

        // Chuyển đổi một chuỗi đầu vào thành một đối tượng Path.
        Path inputPath = Paths.get(args [0]);
        /* Chuyển đổi đầu vào là Path thành một đường dẫn tuyệt đối.*/
        // Nói chung, điều này có nghĩa là thêm vào trước thư mục làm việc hiện thời
        // Nếu ví dụ này được gọi là như thế này:
        //          java FileTest foo
        // thì các phương thức getRoot và getParent sẽ trả về null trên đối tượng "inputPath" gốc
        // Lời gọi tới getRoot và getParent trên đối tượng "fullpath" sẽ trả về giá trị mong muốn

        Path fullpath = inputPath.toAbsolutePath ();
    }
}

Phương thức toAbsolutePath các đầu vào từ người dùng và trả về một đối tượng Path là những giá trị hữu ích khi truy vấn. Các tập tin không cần phải tồn tại cho phương thức này để làm việc.

Phương thức toRealPath trả về một đường dẫn thực sự (real) của tập tin hiện có. Phương thức này thực hiện một số hoạt động như sau:

  • Nếu true được truyền tới với phương thức này và các hệ thống tập tin hỗ trợ các liên kết tượng trưng, thì ​​phương thức này giải quyết bất kỳ liên kết tượng trưng nào trong đường dẫn.
  • Nếu Path là tương đối, nó trả về một đường dẫn tuyệt đối.
  • Nếu Path có chứa bất kỳ thành phần dư thừa nào thì nó sẽ trả về một đường dẫn đã được loại bỏ dư thừa đó.

Phương thức này ném một ngoại lệ nếu các tập tin không tồn tại hoặc không thể truy cập. Bạn có thể bắt ngoại lệ khi bạn muốn để xử lý bất kỳ trường hợp nào. Ví dụ:

try {
    Path fp = path.toRealPath();
} catch(NoSuchFileException x) {
    System.err.format( "%s: không có" + "tập tin hay thư mục nào%n", path;
    // Logic cho trường hợp khi tập tin không tồn tại.
} catch(IOException x) {
    System.err.format( "%s%n", x);
    // logic cho trường hợp lỗi tập tin khác.
}

Nối hai đường dẫn

Ta có thể kết hợp các đường dẫn bằng cách sử dụng phương thức resolve. Khi ta truyền vào một đoạn đường dẫn mà không bao gồm thành phần gốc, thì phần đường dẫn đó sẽ được nối vào đường dẫn gốc.

Ví dụ, hãy xem xét các đoạn mã sau:

// Solaris
Path p1 = Paths.get("/home/joe/ foo");
// Nối thêm bar để được kết quả là /home/joe/foo/ bar
System.out.format("%s%n", p1.resolve("bar"));

//hoặc:

// Microsoft Windows
Path p1 = Paths.get("C:\\home\\joe\\foo");
System.out.format("%sn%", p1.resolve("bar"));

Nếu truyền một đường dẫn tuyệt đối tới phương thức resolve thì nó sẽ trả về chính đường dẫn đó:

// Kết quả là /home/joe
Paths.get("foo").resolve("/home/joe");

Tạo một đường dẫn giữa hai đường dẫn

Một yêu cầu phổ biến khi ta đang viết mã lệnh cho tập tin I/O là khả năng để xây dựng một đường đi từ một vị trí trong hệ thống tập tin đến vị trí khác. Bạn có thể đáp ứng điều này bằng cách sử dụng phương thức relativize. Phương thức này xây dựng một đường dẫn có nguồn gốc từ các đường dẫn ban đầu và kết thúc ở vị trí quy định bởi đường dẫn đã được truyền vào. Đường dẫn mới sẽ là tương đối với đường dẫn ban đầu.

Ví dụ, hãy xét hai đường dẫn tương đối là joe và sally như sau:

Path p1 = Paths.get( "joe");
Path p2 = Paths.get( "sally");

Trong trường hợp không có bất kỳ thông tin nào khác thì ta giả định rằng joe và sally là "anh chị em", có nghĩa chúng là các nút cùng cấp trong cây phân cấp. Để điều hướng từ joe tới sally thì ta sẽ đặt joe là nút cha còn sally là nút con và ngược lại:

// Kết quả là ../sally
Path p1_to_p2 = p1.relativize(p2);
// Kết quả là ../joe
Path p2_to_p1 = p2.relativize (p1);

Hãy xét một ví dụ phức tạp hơn:

Path p1 = Paths.get( "home");
Path p3 = Paths.get( "home/sally/bar");
// Kết quả là sally/bar
Path p1_to_p3 = p1.relativize(p3);
// Kết quả là ../..
Path p3_to_p1 = p3.relativize(p1);

Trong ví dụ này, hai đường dẫn chia sẻ cùng một nút là nút home. Để điều hướng từ home tới bar, trước tiên ta điều hướng home xuống sally rồi sau đó điều hướng tiếp tới bar. Hướng từ bar đến home đòi hỏi phải điều hướng lên hai cấp.

Một đường dẫn tương đối không thể được xây dựng nếu chỉ một trong các đường dẫn chứa thành phần gốc. Nếu cả hai đường dẫn chứa thàn phần gốc thì khả năng xây dựng một đường dẫn tương đối là hệ thống phụ thuộc.

Bạn có thể xem thêm ví dụ đệ quy Copy trong đó có sử dụng các phương thức relativize và resolve.

So sánh hai đường dẫn

Lớp Path hỗ trợ equals, điều này cho phép ta kiểm tra hai đường đường dẫn xem chúng có giống nhau không. Các phương thức startsWith và endsWith cho phép ta kiểm tra xem một đường dẫn có bắt đầu hay kết thúc với một chuỗi cụ thể không. Những phương thức này rất dễ sử dụng. Ví dụ:

Path path = ...;
Path otherPath = ...;
Path beginning = Paths.get("/home");
Path ending = Paths.get("foo");

if(path.equals(otherPath)) {
    / / logic bằng nhau ở đây
} else if (path.startsWith(beginning)) {
    // đường dẫn bắt đầu với "/home"
} else if (path.endsWith(kết thúc)) {
    // đường dẫn kết thúc với "foo"
}

Lớp Path cũng thực thi giao diện Iterable. Phương thức iterator trả về một đối tượng cho phép ta duyệt qua tên của các thành tên trong đường dẫn. Thành phàn đầu tiên trả về là thành phần gần với gốc nhất trong cây thư mục. Đoạn mã sau đây sẽ lặp trên một đường dẫn để in tên của từng thành phần trong đường dẫn đó:

Path path = ...;
for (Path name : path) {
    System.out.println (name);
}

Lớp Path cũng thực thi giao diện Comparable. Ta có thể so sánh các đối tượng Path bằng cách sử dụng phương thức compareTo, nó rất hữu ích cho thao tác sắp xếp. Đương nhiên ta cũng có thể thêm các đối tượng Path vào một tập hợp được (Collection).

Khi ta muốn xác minh rằng hai đối tượng Path cùng định vị trí đến một tập tin, thì ta có thể sử dụng phương thức isSameFile, nó được mô tả trong bài viết Kiểm tra file hoặc thư mục.

Ví dụ lớp Copy

 

import java.nio.file.*;
import static java.nio.file.StandardCopyOption.*;
import java.nio.file.attribute.*;
import static java.nio.file.FileVisitResult.*;
import java.io.IOException;
import java.util.*;
 
/**
 * Sample code that copies files in a similar manner to the cp(1) program.
 */
 
public class Copy {
 
    /**
     * Returns {@code true} if okay to overwrite a  file ("cp -i")
     */
    static boolean okayToOverwrite(Path file) {
        String answer = System.console().readLine("overwrite %s (yes/no)? ", file);
        return (answer.equalsIgnoreCase("y") || answer.equalsIgnoreCase("yes"));
    }
 
    /**
     * Copy source file to target location. If {@code prompt} is true then
     * prompt user to overwrite target if it exists. The {@code preserve}
     * parameter determines if file attributes should be copied/preserved.
     */
    static void copyFile(Path source, Path target, boolean prompt, boolean preserve) {
        CopyOption[] options = (preserve) ?
            new CopyOption[] { COPY_ATTRIBUTES, REPLACE_EXISTING } :
            new CopyOption[] { REPLACE_EXISTING };
        if (!prompt || Files.notExists(target) || okayToOverwrite(target)) {
            try {
                Files.copy(source, target, options);
            } catch (IOException x) {
                System.err.format("Unable to copy: %s: %s%n", source, x);
            }
        }
    }
 
    /**
     * A {@code FileVisitor} that copies a file-tree ("cp -r")
     */
    static class TreeCopier implements FileVisitor<Path> {
        private final Path source;
        private final Path target;
        private final boolean prompt;
        private final boolean preserve;
 
        TreeCopier(Path source, Path target, boolean prompt, boolean preserve) {
            this.source = source;
            this.target = target;
            this.prompt = prompt;
            this.preserve = preserve;
        }
 
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
            // before visiting entries in a directory we copy the directory
            // (okay if directory already exists).
            CopyOption[] options = (preserve) ?
                new CopyOption[] { COPY_ATTRIBUTES } : new CopyOption[0];
 
            Path newdir = target.resolve(source.relativize(dir));
            try {
                Files.copy(dir, newdir, options);
            } catch (FileAlreadyExistsException x) {
                // ignore
            } catch (IOException x) {
                System.err.format("Unable to create: %s: %s%n", newdir, x);
                return SKIP_SUBTREE;
            }
            return CONTINUE;
        }
 
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
            copyFile(file, target.resolve(source.relativize(file)),
                     prompt, preserve);
            return CONTINUE;
        }
 
        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
            // fix up modification time of directory when done
            if (exc == null && preserve) {
                Path newdir = target.resolve(source.relativize(dir));
                try {
                    FileTime time = Files.getLastModifiedTime(dir);
                    Files.setLastModifiedTime(newdir, time);
                } catch (IOException x) {
                    System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x);
                }
            }
            return CONTINUE;
        }
 
        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) {
            if (exc instanceof FileSystemLoopException) {
                System.err.println("cycle detected: " + file);
            } else {
                System.err.format("Unable to copy: %s: %s%n", file, exc);
            }
            return CONTINUE;
        }
    }
 
    static void usage() {
        System.err.println("java Copy [-ip] source... target");
        System.err.println("java Copy -r [-ip] source-dir... target");
        System.exit(-1);
    }
 
    public static void main(String[] args) throws IOException {
        boolean recursive = false;
        boolean prompt = false;
        boolean preserve = false;
 
        // process options
        int argi = 0;
        while (argi < args.length) {
            String arg = args[argi];
            if (!arg.startsWith("-"))
                break;
            if (arg.length() < 2)
                usage();
            for (int i=1; i<arg.length(); i++) {
                char c = arg.charAt(i);
                switch (c) {
                    case 'r' : recursive = true; break;
                    case 'i' : prompt = true; break;
                    case 'p' : preserve = true; break;
                    default : usage();
                }
            }
            argi++;
        }
 
        // remaining arguments are the source files(s) and the target location
        int remaining = args.length - argi;
        if (remaining < 2)
            usage();
        Path[] source = new Path[remaining-1];
        int i=0;
        while (remaining > 1) {
            source[i++] = Paths.get(args[argi++]);
            remaining--;
        }
        Path target = Paths.get(args[argi]);
 
        // check if target is a directory
        boolean isDir = Files.isDirectory(target);
 
        // copy each source file/directory to target
        for (i=0; i<source.length; i++) {
            Path dest = (isDir) ? target.resolve(source[i].getFileName()) : target;
 
            if (recursive) {
                // follow links when copying files
                EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
                TreeCopier tc = new TreeCopier(source[i], dest, prompt, preserve);
                Files.walkFileTree(source[i], opts, Integer.MAX_VALUE, tc);
            } else {
                // not recursive so source must not be a directory
                if (Files.isDirectory(source[i])) {
                    System.err.format("%s: is a directory%n", source[i]);
                    continue;
                }
                copyFile(source[i], dest, prompt, preserve);
            }
        }
    }
}

Các hoạt động với File

Lớp Files là một entrypoint chính của gói java.nio.file. Lớp này cung cấp một tập phong phú các phương thức tĩnh để đọc, ghi, và thao tác với các tập tin và thư mục. Các phương thức của Files làm việc với các đối tượng của lớp Path. Bài viết này ta sẽ tìm hiểu về các chủ đề:

  • Giải phóng tài nguyên hệ thống
  • Bắt ngoại lê
  • Varargs
  • Các hoạt động Atomic
  • Thay đổi phương thức
  • Glob là gì?
  • Nhận diện liên kết

Giải phóng tài nguyên hệ thống

Có khá nhiều tài nguyên được sử dụng trong API này, chẳng hạn như các stream hoặc các channel, thực thi hoặc thừa kế giao diện java.io.Closeable. Một yêu cầu tài nguyên Closeable là phương thức close phải được gọi để giải phóng tài nguyên khi nó không còn cần thiết nữa. Nếu không đóng tài nguyên lại thì có thể ảnh hướng đến hiệu hiệu năng của ứng dụng. Câu lệnh try-with-resources được mô tả trong phần kế tiếp sẽ xử lý các bước này cho ta.

Bắt ngoại lệ

Đối với tập tin I/O, việc xảy ra những điều không mong muốn luôn hiện hữu: một tập tin tồn tại (hoặc không tồn tại) ngoài dự kiến, chương trình không có quyền truy cập vào hệ thống tập tin, hệ thống tập tin mặc định thực hiện không hỗ trợ một chức năng cụ thể, ...

Tất cả các phương thức truy cập vào hệ thống tập tin có thể ném ngoại lệ IOException. Đó là cách tốt nhất để bắt ngoại lệ bằng cách nhúng các phương thức này vào một khối try-with-resources đã được giới thiệu trong phiên bản Java SE 7. Khối try-with-resources có lợi thế là các trình biên dịch tự động tạo code để đóng tài nguyên khi không còn cần thiết. Dưới đây là ví dụ:

Charset charset = Charset.forName( "US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
    writer.write(s, 0, s.length()) ;
} catch(IOException x) {
    System.err.format("IOException: %s%n", x);
}

Ngoài ra, bạn có thể nhúng các phương thức I/O tập tin trong một khối try và sau đó bắt bất kỳ trường hợp ngoại lệ nào trong khối catch. Nếu mã của bạn đã mở bất kỳ một stream hay channel nào thi ta nên đóng chúng trong khối finally. Ví dụ trên sẽ sửa lại như sau:

Charset charset = Charset.forName( "US-ASCII");
String s = ...;
BufferedWriter writer = null;
try {
    writer = Files.newBufferedWriter(file, charset);
    writer.write(s, 0, s.length());
} catch(IOException x) {
    System.err.format("IOException: %sn%", x);
} finally {
    if (write != null)
      writer.close();
}

Ngoài IOException còn có nhiều loại ngoại lệ khác thừa kế từ FileSystemException. Lớp này có một số phương thức hữu ích, chẳng hạn như phương thức getfile cho phép trả lại tập tin đã cung cấp, phương thức getMessage trả về chuỗi tin nhắn chi tiết, getReason trả về lý do tại sao các hoạt động hệ thống tập tin thất bại, ...

Đoạn mã sau đây trình bày cách sử dụng phương thức getfile:

try (...) {
    ...    
} catch(NoSuchFileException x) {
    System.err.format("%s không tồn tại \n", x.getFile());
}

Varargs

Một số phương thức của Files chấp nhận một số lượng tùy ý của các đối số khi các cờ hiệu được quy định. Ví dụ, trong chữ ký phương thức sau đây, ký hiệu 3 chấm (...) đặt sau đối số CopyOption chỉ ra rằng phương thức chấp nhận một số lượng đối số bất ký có kiểu CopyOption, hoặc thường gọi là varargs:

Path Files.move(Path, Path, CopyOption...)

Khi một phương thức chấp nhận một đối số varargs thì ta có thể truyền tới nó một danh sách các giá trị phân cách nhau bằng dấu phẩy hoặc một mảng( CopyOption[]) của các giá trị.

Ví dụ dưới đây cho thấy phương thức move() ở trên có thể được gọi như sau:

import static java.nio.file.StandardCopyOption.*;

Path path = ...;
Path target = ...;
Files.move(path, target, REPLACE_EXISTING, ATOMIC_MOVE);

Các hoạt động Atomic

Cos một số phương thức Files, chẳng hạn như move, có thể thực hiện một số hoạt động atomic trong một số hệ thống tập tin.

Một hoạt động tập tin atomic là một hoạt động mà không bị gián đoạn hoặc chỉ thực hiện được "một phần", hoặc là toàn bộ hoạt động được thực hiện thành công hoặc hoạt động không thành. Điều này rất quan trọng khi ta có nhiều tiến trình hoạt động trên cùng một khu vực của hệ thống tập tin, và ta cần phải đảm bảo rằng mỗi tiến trình phải truy cập một tập tin hoàn chỉnh.

Thay đổi phương thức

Có nhiều phương thức I/O tập tin hỗ trợ phương pháp xâu chuỗi phương thức.

Đầu tiên ta gọi một phương thức và nó trả về một đối tượng, sau đó ta ngay lập tức gọi một phương thức khác trên đối tượng đó, phương thức này cũng trả về một đối tượng nữa, điều này gọi là xâu chuỗi phương thức. Có nhiều ví dụ I/O sử dụng kỹ thuật sau đây:

String value = Charset.defaultCharset().decode(buf).ToString();.
UserPrincipal group = file.
                                getFileSystem().
                                getUserPrincipalLookupService().
                                lookupPrincipalByName ( "tôi");

Kỹ thuật này tạo ra mã nhỏ gọn và cho phép ta tránh được việc phải khai báo các biến tạm mà ta không cần đến sau đó.

Glob là gì?

Hai phương thức trong một lớp Files chấp nhận một đối số glob, nhưng glob là gì?

Ta có thể sử dụng cú pháp glob để xác định dữ liệu có tương thích với mẫu hay không.

Một mẫu glob được quy định như một chuỗi và được ghép nối với các chuỗi khác, chẳng hạn như thư mục hoặc tên tập tin. Cú pháp glob thường có một số quy tắc đơn giản sau:

  • Dấu * : tương ứng với một chuỗi bất kỳ (kể cả chuỗi rỗng).
  • Hai dấu  ** : hoạt động giống như * nhưng vượt qua ranh giới thư mục. Cú pháp này thường được áp dụng với đường dẫn đầy đủ.
  • Dấu ? : tương ứng với một ký tự chính xác.
  • Cặp ngoặc xoắn {} : chỉ định một tập hợp các mẫu con. Ví dụ:
    • {sun,moon,stars} tương đương với "sun", "moon", hoặc "stars".
    • {temp*, tmp*} tương ứng với tất cả các chuỗi bắt đầu với "temp" hoặc "tmp".
  • Cặp ngoặc vuông [] : tương ứng với một tập hợp các ký tự đơn lẻ, khi dấu gạch ngang (-) được sử dụng thì đương đương với một dãy ký tự. Ví dụ:
    • [aeiou] tương ứng với bất kỳ ký tự nguyên âm thường nào.
    • [0-9] tương ứng với một ký số bất kỳ.
    • [A-Z] tương ứng với khớp với bất kỳ ký tự in hoa nào.
    • [a-z,A-Z] tương ứng với bất kỳ ký tự in thường hoặc in hoa nào.
    Trong cặp ngoặc vuông ta cũng có quyền sử dụng những ký tự như *?, và \ để thiết lập mẫu.
  • \ : biến ký tự đặc biệt ngay sau nó thành ký tự thông thường.
  • Tất cả các ký tự còn lại đều tương đương với chính nó (a là a, b là b, ...).
  • Nếu muốn chuyển các ký tự đặc biệt thành ký tự thông thường, ta sử dụng các ký tự sổ ngược đặt trước ký tự đặc biệt đó, ví dụ: \\ tương ứng với một dấu gạch chéo thông thường, \? tương ứng với dấu hỏi, \* tương ứng với dấu hoa thị thông thường.

Dưới đây là một số ví dụ về cách dùng glob:

  • *.html - tương ứng với một chuỗi bất kỳ kết thúc bằng .html
  • ??? - tương ứng với một chuỗi bất kỳ có đúng ba chữ cái hay chữ số (abc, 123,...)
  • *[0-9]* - Tương ứng với một chuỗi bất kỳ có chứa ít nhất một giá trị số
  • *.{htm,html,pdf} - Tương ứng với một chuỗi bất kỳ kết thúc với .htm.html hoặc .pdf
  • a?*.java - Tương ứng với một chuỗi bắt đầu bằng a, tiếp theo là ít nhất một chữ cái hay chữ số, và kết thúc là .java
  • {foo*,*[0-9]*} - Tương ứng với một chuỗi bất kỳ bắt đầu bằng foo hoặc một chuỗi bất kỳ chứa ít nhất một giá trị số.

Lưu ý:  Nếu ta đang gõ những mẫu glob từ bàn phím và nó có chứa một trong các ký tự đặc biệt thì ta phải đặt nó vào cặp nháy kép (chẳng hạn như "*"), sử dụng dấu xổ ngược (\*), hoặc sử dụng bất cứ cơ chế escape được hỗ trợ tại dòng lệnh.

Cú pháp glob rất mạnh mẽ và dễ sử dụng. Tuy nhiên, nếu nó không đáp ứng đầy đủ cho nhu cầu của bạn, bạn cũng có thể sử dụng một biểu thức chính quy (regular expression). Để biết thêm thông tin, xin xem thêm bài viết về Regular Expressions.

Để biết thêm thông tin về các cú pháp glob, xin xem các đặc điểm kỹ thuật API của phương thức getPathMatcher trong lớp FileSystem.

Nhận diện liên kết

Lớp Files là một "nhận diện liên". Theo đó, mỗi một phương thức trong lớp Files hoặc là xác định được phải làm gì khi gặp phải một liên kết tượng trưng, hoặc nó cung cấp một tùy chọn cho phép ta cấu hình các hành vi khi gặp phải liên kết tượng trưng.

Kiểm tra tập tin hoặc thư mục

Bạn có một đối tượng của lớp Path và nó thể hiện một tập tin hoặc một thư mục, nhưng tập tin đó có tồn tại trên hệ thống tập tin? Có thể đọc được không? Có thể ghi được không? Có thể thực thi được không?

Xác minh sự hiện hữu của một tập tin hoặc thư mục

Các phương thức trong lớp Path là cú pháp, có nghĩa là chúng hoạt động trên thể hiện của lớp Path. Nhưng cuối cùng, bạn phải truy cập vào hệ thống tập tin để xác minh rằng Path có tồn tại hay không. Bạn có thể làm như vậy với các phương thức exists(Path, LinkOption ...) và notExists(Path, LinkOption ...). Lưu ý rằng !Files.exists(path) không tương đương với Files.notExists(path). Khi ta đang thử nghiệm sự tồn tại của một tập tin, ba kết quả có thể là:

  • Tập tin được xác định là tồn tại.
  • Tập tin được xác định là không tồn tại.
  • Không rõ trạng thái của tập tin. Kết quả này có thể xảy ra khi các chương trình không có quyền truy cập vào các tập tin.

Nếu cả hai phương thức exists và notExists đều trả về false thì không thể xác định được sự tồn tại của tập tin.

Kiểm tra khả năng truy cập tập tin

Để xác định rằng các chương trình có thể truy cập vào một tập tin khi cần thiết hay không, ta có thể sử dụng các phương thức isReadable(Path)isWritable(Path), và isExecutable(Path).

Đoạn mã dưới đây sẽ kiểm chứng rằng một tập tin cụ thể có tồn tại hay không và rằng chương trình có khả năng thực thi tập tin hay không.

Path file = ...;
boolean isRegularExecutableFile = Files.isRegularFile(file) &
         Files.isReadable(file) & Files.isExecutable(file);

Lưu ý:  Khi bất kỳ của những phương pháp này hoàn tất, không có đảm bảo rằng các tập tin có thể được truy cập. Một lỗ hổng bảo mật thường gặp trong nhiều ứng dụng là để thực hiện một kiểm tra và sau đó truy cập vào các tập tin. Để biết thêm thông tin, sử dụng công cụ tìm kiếm ưa thích của bạn để tìm kiếm TOCTTOU (phát âm TOCK-quá ).

Kiểm tra xem hai đường dẫn có định vị tới cùng một tập tin hay không

Khi bạn có một hệ thống tập tin trong đó có sử dụng các liên kết tượng trưng, thì có thể có hai đường dẫn khác nhau sẽ định vị tới cùng một tập tin. Phương thức isSameFile(Path, Path) sẽ so sánh hai đường dẫn để xác định điều này. Ví dụ:

Path p1 = ...;
Path p2 = ...; 
if (Files.isSameFile(p1, p2)) {
    // Code here
}

Xóa tập tin hoặc thư mục

Ta có thể xóa các tập tin, thư mục hoặc liên kết. Với các liên kết tượng trưng, ​​liên kết sẽ bị xóa và không phải là mục tiêu của liên kết. Đối với các thư mục thì thư mục phải trống, nếu không thì sẽ không xóa được.

Lớp Files cung cấp hai phương thức xóa như sau:

- Phương thức delete(Path) dùng để xóa tập tin hoặc ném một ngoại lệ nếu không xóa được. Ví dụ, nếu tập tin không tồn tại thì ngoại lệ NoSuchFileException sẽ được ném. Ta có thể bắt ngoại lệ để xác định lý do tại sao việc xóa không thành công như sau:

try {
  Files.delete(path);
} catch(NoSuchFileException x) {
  System.err.format("%s: không có " + " tập tin hoặc thư mục%n", path);
} catch (DirectoryNotEmptyException x) {
  System.err.format("%s không trống%n", path);
} catch (IOException x) {
  //Các vấn đề về tập tin được bắt đây.
  System.err.println(x);
}

Phương thức deleteIfExists(Path) cũng xóa các tập tin, nhưng nếu tập tin không tồn tại thì không có ngoại lệ nào được ném ra. Điều này rất hữu ích khi bạn có nhiều luồng để xóa các tập tin và bạn không muốn ném ngoại lệ vì một luồng nào đó đã đảm nhiệm.

Kiểu liệt kê

Một kiểu enum là một kiểu dữ liệu đặc biệt cho phép cho một biến là một tập hợp các hằng số được xác định trước. Các biến phải bằng một trong những giá trị đã được xác định trước cho nó. Ví dụ thường gặp bao gồm hướng la bàn (giá trị của NORTH, SOUTH, EAST và WEST) và các ngày trong tuần.

Bởi vì họ là hằng số, tên của các trường một kiểu enum của những chữ viết hoa.

Trong ngôn ngữ lập trình Java, bạn định nghĩa một kiểu enum bằng enum từ khóa. Ví dụ, bạn sẽ xác định một kiểu enum ngày-of-the-tuần như:

enum công Day { 
    chủ nhật, thứ hai, thứ ba, thứ tư, 
    thứ năm, thứ sáu, thứ bảy 
}

Bạn nên sử dụng các loại enum bất cứ lúc nào bạn cần phải đại diện cho một tập cố định các hằng số. Điều đó bao gồm các loại enum tự nhiên như các hành tinh trong hệ thống và dữ liệu bộ năng lượng mặt trời của chúng tôi, nơi bạn biết tất cả các giá trị có thể tại thời gian biên dịch cho ví dụ, các lựa chọn trên menu, cờ dòng lệnh, và như vậy.

Dưới đây là một số code đó cho bạn thấy làm thế nào để sử dụng ngày enum định nghĩa ở trên:

public class EnumTest { 
    ngày ngày; 
    
    EnumTest công cộng (ngày ngày) { 
        this.day = ngày; 
    } 
    
    public void tellItLikeItIs () { 
        switch (ngày) { 
            Trường hợp thứ hai: 
                System.out.println ( ". Thứ hai là xấu"); 
                nghỉ ; 
                    
            trường hợp thứ sáu: 
                System.out.println ( ". Sáu là tốt hơn"); 
                break; 
                         
            trường hợp thứ bảy: trường hợp Chủ Nhật: 
                System.out.println ( "những ngày nghỉ là tốt nhất."); 
                break; 
                        
            default: 
                System.out.println ( "những ngày giữa tuần là cái như vậy."); 
                break; 
        } 
    } 
    
    public static void main (string [] args) { 
        EnumTest firstDay = new EnumTest (Day.MONDAY); 
        firstDay.tellItLikeItIs (); 
        EnumTest thirdDay = new EnumTest (ngày .WEDNESDAY); 
        thirdDay.tellItLikeItIs (); 
        EnumTest fifthDay = new EnumTest (Day.FRIDAY); 
        fifthDay.tellItLikeItIs (); 
        EnumTest sixthDay = new EnumTest (Day.SATURDAY); 
        sixthDay.tellItLikeItIs (); 
        EnumTest seventhDay = new EnumTest ( Day.SUNDAY); 
        seventhDay.tellItLikeItIs (); 
    } 
}

Đầu ra là:

Thứ Hai là xấu. 
Ngày giữa tuần là cái như vậy. 
Thứ Sáu được tốt hơn. 
Những ngày nghỉ là tốt nhất. 
Những ngày nghỉ là tốt nhất.

Java loại ngôn ngữ lập trình enum là mạnh hơn rất nhiều so với các đối tác của họ trong các ngôn ngữ khác. Các enum khai định nghĩa một lớp (gọi là một kiểu enum ). Cơ thể lớp enum có thể bao gồm các phương pháp và các lĩnh vực khác. Trình biên dịch sẽ tự động thêm một số phương pháp đặc biệt khi nó tạo ra một enum. Ví dụ, họ có một tĩnh giá trị phương thức trả về một mảng chứa tất cả các giá trị của các enum trong thứ tự mà chúng được khai báo. Phương pháp này thường được sử dụng kết hợp với cho-mỗi cấu trúc để lặp qua các giá trị của một kiểu enum. Ví dụ, mã này từ Planet lớp ví dụ dưới đây lặp trên tất cả các hành tinh trong hệ mặt trời.

cho (Planet p: Planet.values ​​()) { 
    System.out.printf ( "trọng lượng của bạn trên% s là% f% n", 
                      p, p.surfaceWeight (khối lượng)); 
}

Lưu ý:  Tất cả các enums ngầm mở rộng java.lang.Enum . Bởi vì một lớp chỉ có thể kéo dài một phụ huynh (xemTuyên bố Classes ), ngôn ngữ Java không hỗ trợ đa kế thừa của nhà nước (xem Nhiều thừa kế của Nhà nước, thực hiện, và Type ), và do đó một enum không thể mở rộng bất cứ điều gì khác.

Trong ví dụ sau, Planet là một kiểu enum đại diện các hành tinh trong hệ mặt trời. Chúng được định nghĩa với khối lượng và bán kính tính liên tục.

Mỗi liên tục enum được khai báo với giá trị cho các thông số khối lượng và bán kính. Những giá trị này được truyền cho constructor khi liên tục được tạo ra. Java đòi hỏi rằng các hằng số được xác định đầu tiên, trước khi bất kỳ lĩnh vực hoặc các phương pháp.Ngoài ra, khi có những lĩnh vực và phương pháp, danh sách các hằng số enum phải kết thúc bằng dấu chấm phẩy.


Lưu ý:  Các nhà xây dựng cho một kiểu enum phải gói riêng hoặc phòng riêng. Nó tự động tạo ra các hằng số được định nghĩa ở phần đầu của cơ thể enum. Bạn không thể gọi một enum xây dựng cho mình.

Ngoài tài sản và xây dựng của nó, Planet có phương pháp cho phép bạn lấy lại trọng lực bề mặt và trọng lượng của một đối tượng trên mỗi hành tinh. Dưới đây là một chương trình mẫu mà mất trọng lượng của bạn trên trái đất (trong bất kỳ đơn vị) và tính toán và in trọng lượng của bạn trên tất cả các hành tinh (trong cùng đơn vị):

enum công Planet { 
    MERCURY (3.303e + 23, 2.4397e6), 
    VENUS (4.869e + 24, 6.0518e6), 
    EARTH (5.976e + 24, 6.37814e6), 
    MARS (6.421e + 23, 3.3972e6), 
    JUPITER ( 1.9e + 27, 7.1492e7), 
    Sao Thổ (5.688e + 26, 6.0268e7), 
    Sao Thiên Vương (8.686e + 25, 2.5559e7), 
    NEPTUNE (1.024e + 26, 2.4746e7); 

    tin cuối cùng đôi khối lượng; // Trong kg 
    tin cuối cùng đôi bán kính; // Trong mét 
    Planet (khối lượng gấp đôi, bán kính gấp đôi) { 
        this.mass = khối lượng; 
        this.radius = bán kính; 
    } 
    tin đôi khối lượng () {return tin đại chúng; } 
    Đôi bán kính nhân () {return bán kính; } 

    // Hằng số hấp dẫn phổ quát (m3 kg-1 s-2) 
    công tĩnh cuối cùng đôi G = 6.67300E-11; 

    surfaceGravity đôi () { 
        trở lại G * khối lượng / (bán kính * bán kính); 
    } 
    surfaceWeight đôi (double otherMass) { 
        trở otherMass * surfaceGravity (); 
    } 
    public static void main (string [] args) { 
        if (args.length = 1) { 
            System.err.println ( "Cách sử dụng: java Planet <earth_weight>"); 
            System.exit ( -1); 
        } 
        đôi earthWeight = Double.parseDouble (args [0]); 
        khối lượng gấp đôi = earthWeight / EARTH.surfaceGravity (); 
        for (Planet p: Planet.values ​​()) 
           System.out.printf ( "trọng lượng của bạn trên % s là% f% n ", 
                             p, p.surfaceWeight (khối lượng)); 
    } 
}

Nếu bạn chạy Planet.class từ dòng lệnh với một đối số của 175, bạn sẽ có được kết quả này:

$ Java Planet 175 
Cân nặng của bạn trên MERCURY là 66,107583 
trọng lượng của bạn trên VENUS là 158,374842 
trọng lượng của bạn trên EARTH là 175,000000 
trọng lượng của bạn trên MARS là 66,279007 
trọng lượng của bạn trên JUPITER là 442,847567 
trọng lượng của bạn trên SATURN là 186,552719 
trọng lượng của bạn trên sao Thiên Vương là 158,397260 
trọng lượng của bạn trên NEPTUNE là 199.207413

Sao chép tập tin hoặc thư mục

Ta có thể sao chép một tập tin hoặc thư mục bằng cách sử dụng phương thức copy(Path, Path, CopyOption ...). Bản sao sẽ lỗi nếu các tập tin đích tồn tại, trừ khi tùy chọn REPLACE_EXISTING được chỉ định.

Thư mục cũng có thể được sao chép. Tuy nhiên, tập tin bên trong thư mục sẽ không được sao chép kèm theo, vì vậy thư mục mới được tạo ra sẽ trống mặc dù thư mục gốc chứa các tập tin.

Khi sao chép một liên kết tượng trưng, ​​đích của liên kết cũng được sao chép. Nếu ta muốn sao chép chính liên kết đó và không phải là nội dung của liên kết thì ta chỉ định tùy chọn NOFOLLOW_LINKS hoặc REPLACE_EXISTING.

Phương thức copy() này có một đối số varargs. Kiểu liệt kê StandardCopyOption và LinkOption được hỗ trợ:

  • REPLACE_EXISTING - Thực hiện các bản sao ngay cả khi các tập tin đích đã tồn tại. Nếu đích là một liên kết tượng trưng thì ​​bản thân liên kết sẽ được sao chép (chứ không phải là đích của liên kết). Nếu đích là một thư mục rỗng thì các bản sao sẽ gặp lỗi do ngoại lệ FileAlreadyExistsException.
  • COPY_ATTRIBUTES - Các bản sao của các thuộc tính tập tin có liên quan đến các tập tin đích. Các thuộc tính tập tin được hỗ trợ là hệ thống tập tin và nền tảng phụ thuộc, nhưng last-modified-time được hỗ trợ trên các nền tảng và được sao chép vào tập tin đích.
  • NOFOLLOW_LINKS - Chỉ ra rằng các liên kết tượng trưng không nên được nối tiếp. Nếu tập tin được sao chép là một liên kết tượng trưng thì liên kết sẽ được sao chép (chứ không phải là đích của liên kết).

Nếu bạn chưa hiểu rõ về kiểu liệt kê, xin xem thêm tại Kiểu liệt kê.

Đoạn mã sau đây trình bày cách sử dụng phương thức copy():

import static java.nio.file.StandardCopyOption.*;
...
Files.copy(source, target, REPLACE_EXISTING);
 

Ngoài sao chép tập tin, lớp Files còn định nghĩa những phương thức mà có thể được sử dụng để sao chép giữa một tập tin và một stream. Phương thức copy(InputStream, Path, CopyOptions...) có thể được sử dụng để sao chép tất cả các byte từ input stream vào tập tin. Phương thức copy(Path, OutputStream) có thể được sử dụng để sao chép tất cả các byte từ một tập tin tới một output stream.

Các ví dụ về lớp Copy sử dụng các phương thức copy() và Files.walkFileTree để hỗ trợ một bản copy đệ quy. Xem Walking the file tree để biết thêm thông tin.

Di chuyển tập tin hoặc thư mục

Bạn có thể di chuyển một tập tin hoặc thư mục bằng cách sử dụng phương thức move(Path, Path, CopyOption...). Việc di chuyển sẽ thất bại nếu tập tin đích tồn tại, trừ khi trùy chọn REPLACE_EXISTING được chỉ định.

Ta cũng có thể di chuyển được thư mục rỗng. Nếu thư mục không rỗng thì việc di chuyển sẽ được cho phép khi thư mục có thể được di chuyển mà không cần di chuyển các nội dung trong thư mục đó. Trên hệ thống UNIX, việc di chuyển một thư mục trong cùng một phân vùng thông thường sẽ bao gồm cả việc đổi tên các thư mục đó. Trong tình hình đó, phương thức move() sẽ được thực hiện ngay cả khi thư mục chứa các tập tin.

move() có một đối số varargs là StandardCopyOption và có những hỗ trợ như sau:

  • REPLACE_EXISTING - Thực hiện di chuyển ngay cả khi các tập tin đích đã tồn tại. Nếu đích là một liên kết tượng trưng thì nó được thay thế nhưng những gì nó trỏ tới không bị ảnh hưởng.

  • ATOMIC_MOVE - Thực hiện việc di chuyển như là một tập tin hoạt động duy nhất. Nếu hệ thống tập tin không hỗ trợ việc di chuyển duy nhất thì một ngoại lệ được ném ra. Với mỗi ATOMIC_MOVE ta có thể di chuyển một tập tin vào một thư mục và được đảm bảo rằng từ thư mục đó ta có thể truy cập vào tập tin một cách bình thường.

Chương trình sau đây minh họa cách sử dụng phương thức move():

import static java.nio.file.StandardCopyOption.*;
...
Files.move(source, target, REPLACE_EXISTING);

Mặc dù bạn có thể thực thi phương thức move() trên một thư mục duy nhất, nhưng phương thức này thường được sử dụng với cơ chế tập cây đệ quy. Để biết thêm thông tin, xem Walking the file tree.

Quản lý siêu dữ liệu (File và File lưu các thuộc tính)

Siêu dữ liệu là "dữ liệu về dữ liệu khác". Với một hệ thống tập tin, dữ liệu được chứa trong các tập tin và thư mục của nó, và siêu dữ liệu theo dõi thông tin về từng đối tượng: Nó là một tập tin chính quy, một thư mục, hay một liên kết? Kích thước của nó, ngày tạo, ngày sửa đổi cuối cùng, chủ sở hữu tập tin, nhóm chủ sở hữu và các quyền hạn truy cập là gì?

Siêu dữ liệu của hệ thống tập tin được thường được tham chiếu như là các thuộc tính tập tin. Lớp Files bao gồm các phương thức có thể được sử dụng để có được một thuộc tính duy nhất của một tập tin, hoặc để thiết lập một thuộc tính.

Phương thức Chú thích
size(Path) Trả về kích thước của tập tin chỉ định trong byte.
isDirectory(Path, LinkOption) Trả về true nếu Path được chỉ định định vị một tập tin đó là một thư mục.
isRegularFile(Path, LinkOption...) Trả về true nếu Path được chỉ định định vị tập tin đó là một tập tin chính quy.
isSymbolicLink(Path) Trả về true nếu Path được chỉ định định vị tập tin đó là một liên kết tượng trưng.
isHidden(Path) Trả về true nếu Path được chỉ định định vị tập tin được xét là ẩn bởi hệ thống tập tin.
getLastModifiedTime(Path, LinkOption...)
setLastModifiedTime(Path, FILETIME)
Trả về hoặc thiết lập thời gian sửa đổi cuối cùng của tập tin được chỉ định.
getOwner(Path, LinkOption...) 
setOwner(Path, UserPrincipal)
Trả về hoặc thiết lập chủ sở hữu của các tập tin.
getPosixFilePermissions(Path, LinkOption...) 
setPosixFilePermissions(Path, Set <PosixFilePermission>)
Trả về hoặc thiết lập quyền truy cập file POSIX của một tập tin.
getAttribute(Path, String, LinkOption...) 
setAttribute(Path, String, Object, LinkOption...)
Trả về hoặt thiết lập giá trị của một thuộc tính tập tin.

Nếu một chương trình cần nhiều thuộc tính tập tin trong cùng khoảng thời gian, thì có thể sẽ không hiệu quả nếu sử dụng các phương thức lấy một thuộc tính đơn. Liên tiếp truy cập vào hệ thống tập tin để lấy một thuộc tính duy nhất có thể ảnh hưởng xấu đến hiệu suất. Vì lý do này, lớp cung cấp hai phương thức readAttributes để lấy các thuộc tính của một tập tin trong một hoạt động với số lượng lớn.

Phương thức Chú thích
readAttributes(Path, String, LinkOption...) Đọc các thuộc tính của một tập tin như là một hoạt động thu thập. Tham số String xác định các thuộc tính được đọc.
readAttributes(Path, Class <A>, LinkOption...) Đọc các thuộc tính của một tập tin như là một hoạt động thập. Tham số Class <A> là các loại thuộc tính được yêu cầu và phương thức trả về một đối tượng của lớp đó.

Trước khi xem ví dụ về các phương thức readAttributes, ta cần biết rằng các hệ thống tập tin khác nhau có những quan niệm khác nhau về những thuộc tính cần được theo dõi. Vì lý do này, các thuộc tính tập tin có liên quan được nhóm lại với nhau thành các view. Một view ánh xạ tới một sự thực thi hệ thống tập tin cụ thể, chẳng hạn như POSIX hay DOS, hoặc đến một chức năng phổ biến, chẳng hạn như quyền sở hữu tập tin.

Dưới đây là các view được hỗ trợ:

  • BasicFileAttributeView - Cung cấp một view cho các thuộc tính cơ bản được yêu cầu để được hỗ trợ bởi tất cả các triển khai hệ thống tập tin.

  • DosFileAttributeView - Mở rộng view thuộc tính cơ bản với bốn bit chuẩn được hỗ trợ trên các hệ thống tập tin có hỗ trợ các thuộc tính hệ điều hành DOS.

  • PosixFileAttributeView - Mở rộng view thuộc tính cơ bản với các thuộc tính hỗ trợ trên hệ thống tập tin hỗ trợ các chuẩn POSIX, chẳng hạn như UNIX. Những thuộc tính này bao gồm chủ sở hữu tập tin, nhóm chủ sở hữu, và chín quyền truy cập có liên quan.

  • FileOwnerAttributeView - Được hỗ trợ bởi bất kỳ sự thực thi hệ thống tập tin nào có hỗ trợ các ý niệm về chủ sở hữu tập tin.

  • AclFileAttributeView - Hỗ trợ đọc hoặc cập nhật danh sách điều khiển truy cập của tập tin (Access Control Lists - ACL). Mô hình ACL NFSv4 cũng được hỗ trợ. Bất kỳ mô hình ACL nào, chẳng hạn như mô hình ACL Windows, mà có một ánh xạ rõ ràng sao cho các mô hình NFSv4 cũng có thể được hỗ trợ.

  • UserDefinedFileAttributeView - Cho phép hỗ trợ siêu dữ liệu mà là người dùng định nghĩa. View này có thể được ánh xạ tới bất kỳ cơ chế mở rộng một hệ thống hỗ trợ nào. Trong hệ điều hành Solaris, bạn có thể sử dụng chế độ này để lưu trữ các kiểu MIME của một tập tin.

Một sự thực thi hệ thống tập tin cụ thể chỉ có thể hỗ trợ loại view tập tin thuộc tính cơ bản, hoặc nó có thể hỗ trợ một số view thuộc tính tập tin này. Một view hệ thống tập tin có thể hỗ trợ các view thuộc tính khác không nằm trong API này.

Trong hầu hết các trường hợp, bạn không cần phải đối phó trực tiếp với bất kỳ một giao diện FileAttributeView nào. Nếu bạn cần phải làm việc trực tiếp với FileAttributeView, bạn có thể truy cập thông qua phương thức getFileAttributeView(Path, Class <V>, LinkOption...).

Phương thức readAttributes sử dụng các generic và có thể được sử dụng để đọc các thuộc tính cho bất kỳ view thuộc tính tập tin nào. Các ví dụ trong phần còn lại của bài viết này sử dụng phương thức readAttributes.

Phần tiếp theo của bài viết bao gồm các chủ đề sau:

  • Các thuộc tính tập tin cơ bản
  • Thiết lập dấu thời gian
  • Thuộc tính tập tin DOS
  • Phân quyền tập tin POSIX
  • Thiết lập một tập tin hoặc chủ sở hữu nhóm
  • Thuộc tính tập tin người dùng định nghĩa
  • Thuộc tính lưu trữ tập tin

Các thuộc tính tập tin cơ bản

Như đã đề cập ở trên, để đọc các thuộc tính cơ bản của một tập tin, ta có thể sử dụng một trong những phương thức Files.readAttributes, đọc tất cả các thuộc tính cơ bản trong một hoạt động hợp nhất. Điều này hiệu quả hơn nhiều so với truy cập vào từng hệ thống tập tin riêng biệt để đọc từng thuộc tính riêng. Đối số varargs hiện tại hỗ trợ kiểu liệt kê LinkOptionNOFOLLOW_LINKS. Sử dụng tùy chọn này khi ta muốn bỏ qua liên kết tượng trưng.


Đôi lời về dấu thời gian:  Tập hợp các thuộc tính cơ bản bao gồm ba dấu thời gian: creationTimelastModifiedTime, và lastAccessTime. Bất kỳ dấu thời gian nào cũng có thể không được hỗ trợ trong việc thực thi cụ thể, trong đó có trường hợp các phương thức truy xuất tương ứng trả về một giá trị thực thi cụ thể. Khi được hỗ trợ, dấu thời gian sẽ được trả về như một đối tượng FILETIME.

Đoạn mã dưới đây đọc và in các thuộc tính tập tin cơ bản cho tập tin đã cho và sử dụng các phương thức của lớp BasicFileAttributes.

Path file = ...;
BasicFileAttributes attr = Files.readAttributes(file, BasicFileAttributes.class);

System.out.println("creationTime: " + attr.creationTime());
System.out.println("lastAccessTime: " + attr.lastAccessTime());
System.out.println("lastModifiedTime: " + attr.lastModifiedTime());

System.out.println("isDirectory: " + attr.isDirectory());
System.out.println("isOther: " + attr.isOther());
System.out.println("isRegularFile: " + attr.isRegularFile());
System.out.println("isSymbolicLink: " + attr.isSymbolicLink());
System.out.println("size: " + attr.size());

 

Ngoài các phương thức accessor thể hiện trong ví dụ trên, có một phương thức có tên fileKey trả về hoặc là một đối tượng xác định duy nhất các tập tin hoặc là rỗng nếu không có sẵn khóa (key) cho tập tin.

Thiết lập dấu thời gian

Đoạn mã sau thiết lập thời gian sửa đổi lần cuối theo đơn vị mini giây:

Path file = ...;
BasicFileAttributes attr =
    Files.readAttributes(file, BasicFileAttributes.class);
long currentTime = System.currentTimeMillis();
FileTime ft = FileTime.fromMillis(currentTime);
Files.setLastModifiedTime(file, ft);
}

 

Thuộc tính tập tin DOS

Các thuộc tính tập tin DOS cũng được hỗ trợ trên hệ thống tập tin khác với DOS, như Samba chẳng hạn. Đoạn mã sau đây sử dụng các phương thức của lớp DosFileAttributes.

Path file = ...;
try {
    DosFileAttributes attr =
        Files.readAttributes(file, DosFileAttributes.class);
    System.out.println("isReadOnly is " + attr.isReadOnly());
    System.out.println("isHidden is " + attr.isHidden());
    System.out.println("isArchive is " + attr.isArchive());
    System.out.println("isSystem is " + attr.isSystem());
} catch (UnsupportedOperationException x) {
    System.err.println("DOS file" +
        " attributes not supported:" + x);
}

Tuy nhiên, bạn có thể thiết lập một thuộc tính DOS bằng cách sử dụng phương thức setAttribute(Path, String, Object, LinkOption...) như sau:

Path file = ...;
Files.setAttribute(file, "dos:hidden", true);

Phân quyền tập tin POSIX

POSIX là cụm từ viết tắt của Portable Operating System Interface for UNIX (Giao diện Hệ điều hành Di động cho UNIX) và là một bộ tiêu chuẩn IEEE và ISO được thiết kế để đảm bảo khả năng tương tác giữa các thành phần khác nhau của UNIX. Nếu một chương trình phù hợp với các tiêu chuẩn POSIX, nó phải dễ dàng được chuyển đến hệ điều hành POSIX khác.

Bên cạnh chủ sở hữu tập tin và chủ sở hữu nhóm, POSIX hỗ trợ chín phân quyền tập tin: các quyền read, write, và execute áp dụng cho chủ sở hữu tập tin, các thành viên trong cùng một nhóm, và những thành viên khác.

Đoạn mã dưới đây đọc các thuộc tính tập tin POSIX cho một tập tin đã cho và in chúng tại output chuẩn. Đoạn mã này sử dụng các phương thức của giao diện PosixFileAttributes.

Path file = ...;
PosixFileAttributes attr =
    Files.readAttributes(file, PosixFileAttributes.class);
System.out.format("%s %s %s%n",
    attr.owner().getName(),
    attr.group().getName(),
    PosixFilePermissions.toString(attr.permissions()));

Lớp trợ giúp PosixFilePermissions cung cấp một số phương thức hữu ích như sau:

  • Phưng thức toString sử dụng trong đoạn mã trên sẽ chuyển các quyền của tập tin thành một chuỗi ký tự (ví dụ, rw-r-r--).

  • Phương thức fromString chấp nhận một chuỗi đại diện cho các quyền của tập tin và xây dựng một tập các quyền tập tin.

  • Phương thức asFileAttribute chấp nhận một tập các quyền của tập tin và xây dựng một thuộc tính tập tin có thể được truyền cho các phương thức Path.createFile hoặc Path.createDirectory.

Đoạn mã dưới đây đọc các thuộc tính từ một tập tin và tạo ra một tập tin mới, gán thuộc tính từ tập tin ban đầu tới các tập tin mới:

Path sourceFile = ...;
Path newFile = ...;
PosixFileAttributes attrs =
    Files.readAttributes(sourceFile, PosixFileAttributes.class);
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(attrs.permissions());
Files.createFile(file, attr);

Phương thức asFileAttribute gộp các quyền cho phép như một FileAttribute. Đoạn mã sau đó cố gắng để tạo ra một tập tin mới theo những quyền này. Lưu ý rằng umask cũng được áp dụng, do đó các tập tin mới có thể được an toàn hơn so với các quyền đã được yêu cầu.

Để thiết lập quyền của một tập tin vào các giá trị đại diện như là một chuỗi mã hóa cứng, ta có thể sử dụng đoạn mã sau:

Path file = ...;
Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.setPosixFilePermissions(file, perms);

Ví dụ Chmod thay đổi các điều khoản của các tập tin theo một cách tương tự như tiện ích chmod.

Thiết lập một tập tin hoặc chủ sở hữu nhóm

Để chuyển một tên thành một đối tượng, ta có thể lưu trữ như là một chủ sở hữu tập tin hoặc một nhóm chủ sở hữu, bạn có thể sử dụng dịch vụ UserPrincipalLookupService. Dịch vụ này sẽ tra một tên hoặc nhóm tên như là một chuỗi và trả về một đối tượng UserPrincipal đại diện cho chuỗi đó. Bạn có thể có được người sử dụng dịch vụ chính cho các hệ thống tập tin mặc định bằng cách sử dụng phương thức FileSystem.getUserPrincipalLookupService.

Đoạn mã sau đây cho thấy cách thiết lập chủ sở hữu tập tin bằng cách sử dụng phương thức setOwner:

Path file = ...;
UserPrincipal owner = file.GetFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByName("sally");
Files.setOwner(file, owner);

Không có phương thức đặc biệt nào trong lớp Files dùng để để thiết lập nhóm chủ sở hữu. Tuy nhiên, có một cách an toàn để làm như vậy một cách trực tiếp, đó là thông qua view thuộc tính tập tin POSIX, như sau:

Path file = ...;
GroupPrincipal group =
    file.getFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByGroupName("green");
Files.getFileAttributeView(file, PosixFileAttributeView.class)
     .setGroup(group);
 

Thuộc tính tập tin người dùng định nghĩa

Nếu các thuộc tính tập tin được hỗ trợ bởi sự thực thi hệ thống tập tin của bạn mà không đủ cho nhu cầu của bạn, thì bạn có thể sử dụng UserDefinedAttributeView để tạo và theo dõi các tập tin thuộc tính của riêng bạn.

Một số sự thực thi sẽ ánh xạ khái niệm này với các đặc điểm như các luồng dữ liệu thay thế NTFS và các thuộc tính mở rộng trên hệ thống tập tin như ext3 và ZFS. Hầu hết các trường áp đặt các hạn chế về kích thước, ví dụ như ext3 giới hạn kích thước là 4 kilobyte (4KB).

Kiểu MIME của một tập tin có thể được lưu trữ như là một thuộc tính người dùng định nghĩa bằng cách sử dụng đoạn mã này:

Path file = ...;
UserDefinedFileAttributeView view = Files
    .getFileAttributeView(file, UserDefinedFileAttributeView.class);
view.write("user.mimetype",
           Charset.defaultCharset().encode("text/html");
 

Để đọc các thuộc tính định dạng MIME, bạn sẽ sử dụng đoạn mã này:

Path file = ...;
UserDefinedFileAttributeView view = Files
.getFileAttributeView(file, UserDefinedFileAttributeView.class);
String name = "user.mimetype";
ByteBuffer buf = ByteBuffer.allocate(view.size(name));
view.read(name, buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();
 

Ví dụ Xdd cho thấy cách làm thế nào để lấy, thiết lập và xóa thuộc tính người dùng định nghĩa.


Lưu ý:  Trong Linux, bạn có thể phải kích hoạt các thuộc tính mở rộng cho các thuộc tính người dùng định nghĩa để làm việc. Nếu bạn nhận được một UnsupportedOperationException khi cố gắng truy cập vào view thuộc tính người dùng định nghĩa, thì bạn cần phải remount các file hệ thống. Các lệnh sau đây sẽ remount phân vùng gốc với các thuộc tính mở rộng cho các hệ thống tập tin ext3 (nếu lệnh này không làm việc với các thành phần khác nhau của Linux, bạn cần tham khảo thêm tài liệu).
$ Sudo mount -o remount, user_xattr /

Nếu bạn muốn thực hiện các thay đổi vĩnh viễn, bạn hãy thêm một điểm đầu vào tới /etc/fstab .


Thuộc tính lưu trữ tập tin

Ta có thể sử dụng lớp FileStore để tìm hiểu thông tin về lưu trữ tập tin, chẳng hạn như còn bao nhiêu không gian có sẵn. Phương thức getFileStore(Path) lấy về file lưu cho tập tin cụ thể.

Đoạn mã sau in ra việc sử dụng không gian cho lưu trữ tập tin, nơi một tập tin cụ thể cư trú:

Path file = ...;
FileStore store = Files.getFileStore(file);

long total = store.getTotalSpace() / 1024;
long used = (store.getTotalSpace() -
             store.getUnallocatedSpace()) / 1024;
long avail = store.getUsableSpace() / 1024;

 

Ví dụ lớp DiskUsage sử dụng API này để in các thông tin về không gian đĩa cho tất cả các vùng lưu trữ trong hệ thống tập tin mặc định.Ví dụ này sử dụng phương thức getFileStores trong lớp FileSystem để lấy tất cả các vùng lưu trữ cho các hệ thống tập tin.

Đọc, ghi và tạo tập tin

Bài viết này thảo luận về các chi tiết của việc đọc, ghi, tạo, và mở tập tin. Ta có rất nhiều các phương thức I/O tập tin để lựa chọn. Để hiểu rõ hơn về API, sơ đồ sau đây sắp xếp các phương thức I/O tập tin theo hướng từ đơn giản đến phức tạp.

Dòng vẽ với tập tin I / O phương pháp sắp xếp từ ít phức tạp (bên trái) đến phức tạp nhất (ở bên phải).
Các phương thức I/O tập tin sắp xếp từ đơn giản đến phức tạp

Ở phía bên trái của sơ đồ là các phương thức tiện ích readAllBytes, readAllLines, và các phương thức write được thiết kế đơn giản, dễ hiểu. Phía bên phải là những phương thức được sử dụng để lặp qua một luồng hoặc các dòng văn bản, chẳng hạn như newBufferedReader, newBufferedWriter, sau đó là newInputStream và newOutputStream. Những phương thức này tương thích với gói java.io. Hướng tiếp về phía bên phải là những phương thức để giao dịch với ByteChannels, SeekableByteChannels, và ByteBuffers, chẳng hạn như phương thức newByteChannel. Cuối cùng là những phương thức sử dụng FileChannel cho các ứng dụng tiên tiến cần khóa tập tin hoặc ánh xạ bộ nhớ I/O.

Lưu ý:  Các phương thức tạo ra một tập tin mới này cho phép bạn chỉ định một tập tùy chọn các thuộc tính khởi tạo cho các tập tin. Ví dụ, trên một hệ thống tập tin hỗ trợ tập chuẩn POSIX (như UNIX), bạn có thể chỉ định một chủ sở hữu tập tin, nhóm chủ sở hữu hoặc quyền tập tin tại thời điểm các tập tin được tạo ra.

Chủ đề của bài viết:

  • Tham số OpenOptions
  • Các phương pháp thường được sử dụng cho tập tin nhỏ
  • Các phương thức I/O đệm cho các tập tin văn bản
  • Phương thức Unbuffered Streams và Interoperable với API java.io
  • Phương cho Channel và ByteBuffers
  • Phương thức tạo tập tin thường xuyên và tạm thời

Tham số OpenOptions

Một số phương thức trong phần này có một tùy chọn là tham số OpenOptions. Tham số này là tùy chọn và API sẽ cung cấp cho bạn biết các hành vi mặc định dành cho phương thức khi nó không được xác định.

Những liệt kê StandardOpenOptions sau đây được hỗ trợ:

  • WRITE - Mở tập tin để ghi.
  • APPEND - Đưa thêm dữ liệu mới vào cuối của tập tin. Tùy chọn này được sử dụng với tùy chọn WRITE hoặc CREATE.
  • TRUNCATE_EXISTING - Cắt tập tin thành 0 byte. Tùy chọn này được sử dụng với tùy chọn WRITE.
  • CREATE_NEW - Tạo ra một tập tin mới và ném một ngoại lệ nếu các tập tin đã tồn tại.
  • CREATE - Mở tập tin nếu nó tồn tại hoặc tạo ra một tập tin mới nếu nó không tồn tại.
  • DELETE_ON_CLOSE - Xóa tập tin khi đóng luồng. Tùy chọn này rất hữu ích cho các tập tin tạm thời.
  • SPARSE - Gợi ý rằng một tập tin mới được tạo ra sẽ được phân tách. Tùy chọn nâng cao này được dùng trên một số hệ thống tập tin, chẳng hạn như NTFS, nơi các tập tin lớn với "khoảng trống" dữ liệu có thể được lưu trữ một cách hiệu quả hơn, nơi những khoảng trống không tiêu thụ không gian đĩa.
  • SYNC - Giữ các tập tin (cả về nội dung và siêu dữ liệu) đồng bộ với các thiết bị lưu trữ bên dưới.
  • DSYNC - Giữ các nội dung tập tin đồng bộ hóa với các thiết bị lưu trữ bên dưới.

Các phương thức thường được sử dụng cho tập tin nhỏ

Đọc tất cả Bytes hoặc Lines từ một tập tin

Nếu bạn có một tập tin nhỏ và bạn muốn đọc toàn bộ nội dung của nó trong một lần truyền, bạn có thể sử dụng phương thức readAllBytes(Path) hoặc readAllLines(Path, Charset). Những phương thức này thực hiện hầu hết các công việc cho bạn, chẳng hạn như mở và đóng luồng, nhưng không có ý định để xử lý các tập tin lớn. Đoạn mã sau đây cho thấy cách sử dụng phương thức readAllBytes:

Path file = ...;
byte[] fileArray;
fileArray = Files.readAllBytes(file);

Viết tất cả các Byte hoặc hoặc các dòng ra tập tin

Bạn có thể sử dụng một trong những phương thức để ghi byte hoặc dòng vào một tập tin.

  • write(Path, byte[], OpenOption...)
  • write(Path, Iterable< extends CharSequence>, Charset, OpenOption...)

Đoạn mã sau đây cho thấy cách dùng phương thức write().

Path file = ...;
byte[] buf = ...;
Files.write(file, buf);

Các phương thức I/O đệm cho các tập tin văn bản

Gói java.nio.file hỗ trợ kênh I/O để di chuyển dữ liệu trong bộ đệm, bỏ qua một số lớp có thể hạn chế luồng I/O.

Đọc một tập tin bằng cách sử dụng luồng I/O đệm

Phương thức newBufferedReader(Path, Charset) mở một tập tin để đọc, trả lại một BufferedReader có thể được sử dụng để đọc văn bản từ một tập tin một cách hiệu quả.

Đoạn mã sau đây cho thấy cách sử dụng phương thức newBufferedReader để đọc từ một tập tin. Các tập tin được mã hóa thành dạng "US-ASCII".

Charset charset = Charset.forName("US-ASCII");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

Ghi một tập tin bằng cách sử dụng luồng I/O đệm

Bạn có thể sử dụng phương thức newBufferedWriter(Path, Charset, OpenOption ...) để ghi vào một tập tin bằng cách sử dụng BufferedWriter.

Đoạn mã sau đây cho thấy cách tạo ra một tập tin mã hóa dạng "US-ASCII" sử dụng phương thức này:

Charset charset = Charset.forName("US-ASCII");
String s = ...;
try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
    writer.write(s, 0, s.length());
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

Phương thức Unbuffered Streams và Interoperable với API java.io

Đọc một tập tin bằng cách sử dụng luồng I/O

Để mở một tập tin để đọc, bạn có thể sử dụng phương thức newInputStream(Path, OpenOption ...). Phương thức này trả về một dòng đầu vào không có bộ đệm để đọc byte từ tập tin.

Path file = ...;
try (InputStream in = Files.newInputStream(file);
    BufferedReader reader =
      new BufferedReader(new InputStreamReader(in))) {
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException x) {
    System.err.println(x);
}

Tạo và ghi một tập tin bằng cách sử dụng luồng I/O

Bạn có thể tạo một tập tin, thêm vào một tập tin, hoặc ghi vào một tập tin bằng cách sử dụng phương thức newOutputStream(Path, OpenOption ...). Phương thức này sẽ mở ra hoặc tạo ra một tập tin để ghi các byte và trả về một dòng đầu ra không có bộ đệm.

Phương thức này có một tham số tùy chọn là OpenOption. Nếu tùy chọn này không được dùng, và tập tin không tồn tại, thì một tập tin mới sẽ được tạo ra. Nếu tập tin tồn tại thì nó sẽ được cắt ngắn. Tùy chọn này tương đương với cách gọi phương thức với tùy chọn CREATE và TRUNCATE_EXISTING.

Ví dụ sau đây sẽ mở ra một file log. Nếu tập tin không tồn tại, nó sẽ được tạo. Nếu tập tin tồn tại, nó được mở ra để thêm dữ liệu.

import static java.nio.file.StandardOpenOption.*;
import java.nio.file.*;
import java.io.*;

public class LogFileTest {

  public static void main(String[] args) {

    // Convert the string to a
    // byte array.
    String s = "Hello World! ";
    byte data[] = s.getBytes();
    Path p = Paths.get("./logfile.txt");

    try (OutputStream out = new BufferedOutputStream(
      Files.newOutputStream(p, CREATE, APPEND))) {
      out.write(data, 0, data.length);
    } catch (IOException x) {
      System.err.println(x);
    }
  }
}

Phương cho Channel và ByteBuffers

Đọc và ghi tập tin bằng cách sử dụng kênh I/O

Trong khi luồng I/O đọc một ký tự tại một thời thời điểm, thì kênh I/O đọc một bộ đệm tại một thời điểm. Giao diện ByteChannel cung cấp các chức năng cơ bản là read và write. SeekableByteChannel là một ByteChannel có khả năng duy trì một vị trí trong kênh và thay đổi vị trí đó. SeekableByteChannel cũng hỗ trợ cắt bỏ các tập tin liên kết với kênh và truy vấn các tập tin với kích thước của nó.

Khả năng di chuyển đến các điểm khác nhau trong tập tin và sau đó đọc từ hay ghi vào vị trí đó sẽ dẫn đến truy cập ngẫu nhiên một tập tin.

Có hai phương thức để đọc và ghi kênh I/.

  • newByteChannel(Path, OpenOption...)
  • newByteChannel(Path, Set<? extends OpenOption>, FileAttribute<?>...)
Lưu ý:  Các newByteChannel phương pháp trả về một thể hiện của một SeekableByteChannel . Với một hệ thống tập tin mặc định, bạn có thể cast kênh này byte seekable một FileChannel cung cấp quyền truy cập vào nhiều tính năng tiên tiến như lập bản đồ một khu vực của các tập tin trực tiếp vào bộ nhớ truy cập nhanh hơn, khóa một khu vực của các tập tin để các quá trình khác không thể truy cập vào nó, hoặc đọc và viết byte từ một vị trí tuyệt đối mà không ảnh hưởng đến vị trí hiện tại của kênh.

Cả hai phương thức newByteChannel trên đều cho phép bạn chỉ định một danh sách các tùy chọn OpenOption. Các tùy chọn mở tương tự được sử dụng bởi phương thức newOutputStream cũng được hỗ trợ, ngoài việc thêm một tùy chọn: READ được yêu cầu bởi vì SeekableByteChannel hỗ trợ cả đọc và ghi.

Theo quy định thì READ sẽ mở kênh để đọc, WRITE hoặc APPEND mở kênh để ghi. Nếu không có tùy chọn nào được chỉ định, thì kênh sẽ được mở ra để đọc.

Đoạn mã dưới đây đọc một tập tin và in nó vào đầu ra tiêu chuẩn:

// Defaults to READ
try (SeekableByteChannel sbc = Files.newByteChannel(file)) {
    ByteBuffer buf = ByteBuffer.allocate(10);

    // Read the bytes with the proper encoding for this platform.  If
    // you skip this step, you might see something that looks like
    // Chinese characters when you expect Latin-style characters.
    String encoding = System.getProperty("file.encoding");
    while (sbc.read(buf) > 0) {
        buf.rewind();
        System.out.print(Charset.forName(encoding).decode(buf));
        buf.flip();
    }
} catch (IOException x) {
    System.out.println("caught exception: " + x);

Ví dụ sau đây viết cho UNIX và hệ thống tập tin POSIX khác, tạo ra một tập tin đăng nhập với một tập hợp cụ thể các quyền tập tin. Đoạn mã tạo một tập tin đăng nhập hoặc đưa thêm vào file log nếu nó đã tồn tại. file log được tạo ra với quyền đọc/ghi cho chủ sở hữu và quyền chỉ đọc cho nhóm.

import static java.nio.file.StandardOpenOption.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

public class LogFilePermissionsTest {

  public static void main(String[] args) {

    // Create the set of options for appending to the file.
    Set<OpenOption> options = new HashSet<OpenOption>();
    options.add(APPEND);
    options.add(CREATE);

    // Create the custom permissions attribute.
    Set<PosixFilePermission> perms =
      PosixFilePermissions.fromString("rw-r-----");
    FileAttribute<Set<PosixFilePermission>> attr =
      PosixFilePermissions.asFileAttribute(perms);

    // Convert the string to a ByteBuffer.
    String s = "Hello World! ";
    byte data[] = s.getBytes();
    ByteBuffer bb = ByteBuffer.wrap(data);

    Path file = Paths.get("./permissions.log");

    try (SeekableByteChannel sbc =
      Files.newByteChannel(file, options, attr)) {
      sbc.write(bb);
    } catch (IOException x) {
      System.out.println("Exception thrown: " + x);
    }
  }
}

Phương thức tạo tập tin thường xuyên và tạm thời

Tạo tập tin

Bạn có thể tạo một tập tin rỗng với một tập giá trị khởi tạo cho các thuộc tính bằng cách sử dụng phương thức CreateFile(Path, FileAttribute <?>). Ví dụ, nếu vào thời điểm tạo bạn muốn có một tập tin để có một tập hợp các quyền tập tin, thì bạn sử dụng phương thức CreateFile để thực hiện. Nếu bạn không chỉ định bất kỳ các thuộc tính nào, thì tập tin sẽ được tạo ra với các thuộc tính mặc định. Nếu tập tin đã tồn tại, thì CreateFile sẽ ném một ngoại lệ.

Trong một hoạt động atomic đơn, phương thức CreateFile sẽ kiểm tra sự tồn tại của tập tin và tạo tập tin đó với các thuộc tính được chỉ định, điều này sẽ làm cho tiến trình an toàn hơn chống lại các mã độc hại.

Đoạn mã sau tạo một tập tin với các thuộc tính mặc định:

Path file = ...;
try {
    // Create the empty file with default permissions, etc.
    Files.createFile(file);
} catch (FileAlreadyExistsException x) {
    System.err.format("file named %s" +
        " already exists%n", file);
} catch (IOException x) {
    // Some other sort of failure, such as permissions.
    System.err.format("createFile error: %s%n", x);
}

Quyền tập tin POSIX có một ví dụ sử dụng CreateFile(Path, FileAttribute <?>) để tạo ra một tập tin với quyền thiết lập được tạo sẵn.

Bạn cũng có thể tạo một file mới bằng cách sử dụng phương thức newOutputStream. Nếu bạn mở một luồng đầu ra và đóng nó ngay lập tức, thì một tập tin rỗng sẽ được tạo ra.

Tạo tập tin tạm thời

Bạn có thể tạo một tập tin tạm thời bằng cách sử dụng một trong các phương thức createTempFile sau:

  • createTempFile(Path, String, String, FileAttribute <?>)
  • createTempFile(String, String, FileAttribute <?>)

Phương thức đầu tiên cho phép mã lệnh chỉ định một thư mục cho các tập tin tạm thời và phương thức thứ hai tạo ra một file mới trong thư mục tập tin-tạm thời mặc định. Cả hai phương thức đều cho phép bạn chỉ định một hậu tố cho tên tập tin và phương thức đầu tiên cho phép bạn cũng chỉ định một tiền tố. Đoạn mã dưới đây thể hiện cách dùng hai phương thức thứ hai:

try {
    Path tempFile = Files.createTempFile(null, ".myapp");
    System.out.format("The temporary file" +
        " has been created: %s%n", tempFile)
;
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

Kết quả của việc chạy tập tin này sẽ là như sau:

The temporary file has been created: /tmp/509668702974537184.myapp

Định dạng cụ thể của tên tập tin tạm thời là nền tảng cụ thể.

Tập tin truy cập ngẫu nhiên

File truy cập ngẫu nhiên cho phép tính không cần tuần tự trong việc truy cập vào nội dung của một tập tin. Để truy cập vào một tập tin ngẫu nhiên, bạn mở tập tin, tìm kiếm một vị trí cụ thể, và đọc từ hay ghi vào tập tin đó.

Chức năng này là có thể thực hiện được với giao diện SeekableByteChannel. Giao diện SeekableByteChannel mở rộng kênh I/O với các khái niệm về vị trí hiện tại. Các phương thức cho phép bạn thiết lập hoặc truy vấn các vị trí, và sau đó bạn có thể đọc dữ liệu từ, hoặc ghi dữ liệu vào vị trí đó. Các API bao gồm một số ít phương thức dễ sử dụng như sau:

  • position - Trả về vị trí hiện tại của kênh
  • position(long) - Thiết lập vị trí của kênh
  • read(ByteBuffer) - Đọc byte vào bộ đệm từ kênh
  • write(ByteBuffer) - Ghi byte từ bộ đệm vào kênh
  • truncate(long) - Cắt các file (hoặc đầu vào khác) kết nối với kênh

Đọc và ghi tập tin với kênh I/O cho thấy phương thức Path.newByteChannel trả về một thể hiện của SeekableByteChannel. Trên hệ thống tập tin mặc định, bạn có thể sử dụng kênh đó như thế, hoặc bạn có thể ép nó thành một FileChannel để có thể truy cập vào các tính năng tiên tiến hơn, chẳng hạn như lập bản đồ một khu vực của các tập tin trực tiếp vào bộ nhớ truy cập nhanh hơn, khóa một khu vực của tập tin, hoặc đọc và viết byte từ một vị trí tuyệt đối mà không ảnh hưởng đến vị trí hiện tại của kênh.

Đoạn mã sau sẽ mở một tập tin để đọc và ghi bằng cách sử dụng một trong những phương thức newByteChannelSeekableByteChannel mà được trả về sẽ được ép thành FileChannel. Sau đó, 12 byte được đọc từ đầu của tập tin, và chuỗi "I was here!" được viết ở vị trí đó. Vị trí hiện tại trong tập tin được chuyển đến cuối tập tin, và 12 byte từ đầu được nối. Cuối cùng, chuỗi "I was here!" được nối và các kênh trên các tập tin được đóng.

String s = "I was here!\n";
byte data[] = s.getBytes();
ByteBuffer out = ByteBuffer.wrap(data);

ByteBuffer copy = ByteBuffer.allocate(12);

try (FileChannel fc = (FileChannel.open(file, READ, WRITE))) {
    // Read the first 12
    // bytes of the file.
    int nread;
    do {
        nread = fc.read(copy);
    } while (nread != -1 && copy.hasRemaining());

    // Write "I was here!" at the beginning of the file.
    fc.position(0);
    while (out.hasRemaining())
        fc.write(out);
    out.rewind();

    // Move to the end of the file.  Copy the first 12 bytes to
    // the end of the file.  Then write "I was here!" again.
    long length = fc.size();
    fc.position(length-1);
    copy.flip();
    while (copy.hasRemaining())
        fc.write(copy);
    while (out.hasRemaining())
        fc.write(out);
} catch (IOException x) {
    System.out.println("I/O Exception: " + x);
}

Tạo và đọc thư mục

Trong một số phương thức đã trình bày ở những bài viết trước, chẳng hạn như phương thức delete(), nó làm việc trên các tập tin, liên kết  thư mục. Nhưng làm thế nào để bạn liệt kê tất cả các thư mục ở trên cùng của một hệ thống tập tin? Làm thế nào để bạn xem nội dung của một thư mục hoặc tạo một thư mục?

Bài viết này bao gồm các trình bày cụ thể sau đây cho thư mục:

  • Liệt kê thư mục gốc của một hệ thống tập tin
  • Tạo một thư mục
  • Tạo một thư mục tạm thời
  • Liệt kê các nội dung của thư mục
  • Lọc danh sách thư mục bằng cách sử dụng globbing
  • Ghi bộ lọc thư mục riêng của bạn

Liệt kê thư mục gốc của một hệ thống tập tin

Bạn có thể liệt kê tất cả các thư mục gốc cho một hệ thống tập tin bằng cách sử dụng phương thức FileSystem.getRootDirectories. Phương thức này trả về một Iterable, cho phép bạn sử dụng vòng lặp for cải tiến để lặp qua tất cả các thư mục gốc.

Đoạn mã sau in ra các thư mục gốc cho các hệ thống tập tin mặc định:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
for (Path name: dirs) {
    System.err.println(name);
}

Tạo một thư mục

Bạn có thể tạo một thư mục mới bằng cách sử dụng phương thức createDirectory(Path, FileAttribute <?>). Nếu bạn không chỉ định thuộc tính FileAttributes nào, thì thư mục mới sẽ có các thuộc tính mặc định. Ví dụ:

Path dir = ...;
Files.createDirectory(path);

Đoạn mã sau tạo một thư mục mới trên một hệ thống tập tin POSIX có quyền truy cập cụ thể:

Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.createDirectory(file, attr);

Để tạo một thư mục nhiều cấp độ khi một hoặc nhiều thư mục cha có thể chưa tồn tại, bạn có thể sử dụng phương thức createDirectories(Path, FileAttribute <?>). Như với phương thức createDirectory(Path, FileAttribute <?>), bạn có thể chỉ định một bộ tùy chọn các thuộc tính tập tin ban đầu. Đoạn mã sau đây sử dụng các thuộc tính mặc định:

Files.createDirectories(Paths.get("foo/bar/test"));

Các thư mục được tạo ra theo ý muốn, từ trên xuống dưới, trong ví dụ trên sẽ là foo/bar/test, nếu thư mục foo thư mục không tồn tại thì nó được tạo ra. Tiếp theo, thư mục bar được tạo ra, và, cuối cùng, thư mục test được tạo ra.

Có thể phương thức trên sẽ không thực hiện được sau khi tạo một số, nhưng không phải tất cả các thư mục mẹ.

Tạo thư mục tạm thời

Bạn có thể tạo một thư mục tạm thời sử dụng một trong các phương thức createTempDirectory:

  • createTempDirectory(Path, String, FileAttribute <?> ...)
  • createTempDirectory(String, FileAttribute <?> ...)

Phương thức đầu tiên cho phép mã lệnh xác định vị trí cho thư mục tạm thời và phương thức thứ hai tạo ra một thư mục mới trong thư mục tạm thời mặc định.

Liệt kê các nội dung của thư mục

Bạn có thể liệt kê tất cả các nội dung của một thư mục bằng cách sử dụng phương thức newDirectoryStream(Path). Phương thức này trả về một đối tượng để thực thi giao diện DirectoryStream giao diện. Lớp thực thi giao diện DirectoryStream cũng thực hiện Iterable, vì vậy bạn có thể lặp qua luồng thư mục để đọc tất cả các đối tượng. Cách tiếp cận này phù hợp với quy mô thư mục rất lớn.


Hãy nhớ rằng:  Giá trị trả về của DirectoryStream là một luồng. Nếu bạn không sử dụng try-with-resources, thì bạn nhớ đóng luồng trong khối finally. Các try-with-resources thực hiện điều này cho bạn.

Đoạn mã sau đây cho thấy cách in nội dung của một thư mục:

Path dir = ...;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path file: stream) {
        System.out.println(file.getFileName());
    }
} catch (IOException | DirectoryIteratorException x) {
    // IOException can never be thrown by the iteration.
    // In this snippet, it can only be thrown by newDirectoryStream.
    System.err.println(x);
}

Các đối tượng Path sẽ được trả về bởi các iterator là tên của các mục giải quyết đối với các thư mục. Vì vậy, nếu bạn định liệt kê các nội dung của thư mục /tmp thì các mục sẽ được trả về có dạng /tmp/a/tmp/b, ...

Phương thức này sẽ trả vè toàn bộ nội dung của một thư mục: các tập tin, liên kết, thư mục con, và các tập tin ẩn. Nếu bạn muốn có thêm nhiều lựa chọn về các nội dung được lấy ra, bạn có thể sử dụng một trong những phương thức newDirectoryStream khác, như mô tả phía dưới.

Lưu ý rằng nếu có một ngoại lệ khi lặp thư mục thì DirectoryIteratorException được ném với IOException là nguyên nhân, bởi vì các phương thức lặp không thể ném ngoại lệ.

Lọc danh sách thư mục bằng cách sử dụng globbing

Nếu bạn muốn lấy những file và thư mục con trong đó mỗi tên phù hợp với một mẫu cụ thể, thì bạn có thể làm như sau bằng cách sử dụng phương thức newDirectoryStream(Path, String), trong đó cung cấp một bộ lọc glob được xây dựng sẵn.

Ví dụ, đoạn mã sau đây liệt kê các tập tin liên quan đến Java: .class , .java và .jar:

Path dir = ...;
try (DirectoryStream<Path> stream =
     Files.newDirectoryStream(dir, "*.{java,class,jar}")) {
    for (Path entry: stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException x) {
    // IOException can never be thrown by the iteration.
    // In this snippet, it can // only be thrown by newDirectoryStream.
    System.err.println(x);
}

Ghi bộ lọc thư mục riêng của bạn

Nếu bạn muốn lọc các nội dung của một thư mục dựa trên một số điều kiện khác so với mô hình kết hợp, bạn có thể tạo bộ lọc riêng của bạn bằng cách thực thi giao diện DirectoryStream.Filter<T>. Giao diện này bao gồm một phương thức là accept, nó sẽ xác định xem một tập tin có đáp ứng các yêu cầu tìm kiếm hay không.

Ví dụ, đoạn mã sau đây thực hiện một bộ lọc chỉ để lấy các thư mục:

DirectoryStream.Filter<Path> filter =
    newDirectoryStream.Filter<Path>() {
    public boolean accept(Path file) throws IOException {
        try {
            return (Files.isDirectory(path));
        } catch (IOException x) {
            // Failed to determine if it's a directory.
            System.err.println(x);
            return false;
        }
    }
};

 

Khi bộ lọc đã được tạo ra, nó có thể được gọi bằng cách sử dụng phương thức newDirectoryStream(Path, DirectoryStream.Filter<? super Path>). Đoạn mã sau đây sử dụng bộ lọc isDirectory để chỉ in các thư mục con của thư mục tới đầu ra chuẩn:

Path dir = ...;
try (DirectoryStream<Path>
                       stream = Files.newDirectoryStream(dir, filter)) {
    for (Path entry: stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException x) {
    System.err.println(x);
}

Phương thức này được sử dụng chỉ để lọc một thư mục duy nhất. Tuy nhiên, nếu bạn muốn tìm tất cả các thư mục con trong một cây tập tin, bạn sẽ sử dụng cơ chế Đi bộ qua cây tập tin.

Liên kết, liên kết tượng trưng hay không phải

Như đã đề cập ở những bài viết trước, gói java.nio.file và lớp Path là "liên kết nhận thức". Mỗi phương thức Path hoặc là phát hiện phải làm gì khi một liên kết tượng trưng là gặp phải, hoặc nó cung cấp một tùy chọn cho phép bạn cấu hình các hành vi khi gặp một liên kết tượng trưng.

Các bài viết thảo luận cho đến nay đã nói về biểu tượng hay liên kết mềm, nhưng một số hệ thống tập tin cũng hỗ trợ các liên kết cứng. Liên kết cứng có nhiều hạn chế hơn so với các liên kết tượng trưng, cụ thể ​​như sau:

  • Đích của liên kết phải tồn tại.
  • Liên kết cứng nói chung là không được phép trên các thư mục.
  • Liên kết cứng không được phép vượt qua các phân vùng. Do đó, chúng không thể tồn tại trên hệ thống tập tin.
  • Một liên kết cứng có vẻ như hoạt động như một tập tin thường xuyên, vì vậy nó có thể khó tìm thấy.
  • Một liên kết cứng xét về ý nghĩa và mục đích, thì cùng một thực thể giống như file gốc. Nó có cùng các quyền tập tin, tem thời gian, và như vậy. Tất cả các thuộc tính giống hệt nhau.

Do những hạn chế, liên kết cứng không được sử dụng như là các liên kết tượng trưng, ​​nhưng phương thức Path làm việc ổn định hơn so với liên kết cứng.

Một số mục trong bài viết này là như sau:

  • Tạo một liên kết tượng trưng
  • Tạo một liên kết cứng
  • Phát hiện liên kết tượng trưng
  • Tìm đích của liên kết

Tạo một liên kết tượng trưng

Nếu hệ thống tập tin của bạn hỗ trợ, bạn có thể tạo một liên kết tượng trưng bằng phương thức createSymbolicLink(Path, Path, FileAttribute<?>). Đối số Path thứ hai thể hiện tập tin hoặc thư mục đích và có thể hoặc không tồn tại. Đoạn mã sau tạo một liên kết tượng trưng với các quyền mặc định:

Path newLink = ...;
Path target = ...;
try {
    Files.createSymbolicLink(newLink, target);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not support symbolic links.
    System.err.println(x);
}

vararg FileAttributes cho phép bạn chỉ định các thuộc tính tập tin ban đầu được thiết lập nguyên tử khi liên kết được tạo ra. Tuy nhiên, đối số này được dự định để sử dụng trong tương lai và hiện tại chưa được thực hiện.

Tạo một liên kết cứng

Bạn có thể tạo một liên kết cứng (hoặc liên kết thường xuyên) đến một tập tin hiện có bằng cách sử dụng phương thức CreateLink(Path, Path). Đối số Path thứ hai là đường dẫn tới tập tin hiện có, và nó phải tồn tại, nếu không thì một NoSuchFileException được ném. Đoạn mã sau đây cho thấy cách tạo ra một liên kết:

Path newLink = ...;
Path existingFile = ...;
try {
    Files.createLink(newLink, existingFile);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not
    // support adding an existing
    // file to a directory.
    System.err.println(x);
}

Phát hiện liên kết tượng trưng

Để xác định liệu một thể hiện Path nào đó có là một liên kết tượng trưng hay không, ​​bạn có thể sử dụng phương thức isSymbolicLink(Path). Đoạn mã sau đây cho thấy cách làm:

Path file = ...;
boolean isSymbolicLink =
    Files.isSymbolicLink(file);

Tìm đích của liên kết

Bạn có thể có được đích của một liên kết tượng trưng bằng phương thức readSymbolicLink(Path), cụ thể như sau:

You can obtain the target of a symbolic link by using the readSymbolicLink(Path) method, as follows:

Path link = ...;
try {
    System.out.format("Target of link" +
        " '%s' is '%s'%n", link,
        Files.readSymbolicLink(link));
} catch (IOException x) {
    System.err.println(x);
}

Nếu Path không phải là một liên kết tượng trưng thì ​​phương thức này sẽ ném một NotLinkException.

Đi bộ qua cây tập tin

Bạn cần tạo ra một ứng dụng để đệ quy thăm tất cả các tập tin trong một cây tập tin? Bạn có thể thực hiện bằng cách xóa tất cả các tập tin có đuôi .class trong một cây, hoặc tìm tất cả các tập tin không được truy cập trong một năm. Bạn có thể làm như vậy với giao diện FileVisitor.

Phần này bao gồm những vấn đề sau đây:

  • Giao diện FileVisitor
  • Tiến trình Kickstarting
  • Những điều cần tính đến khi tạo một FileVisitor
  • Kiểm soát dòng chảy

Giao diện FileVisitor

Để đi bộ qua một cây tập tin, trước tiên bạn cần phải thực thi giao diện FileVisitor. Một FileVisitor quy định các hành vi cần thiết tại các điểm quan trọng trong quá trình truyền: khi một tập tin được truy cập, trước khi một thư mục được truy cập, sau khi một thư mục được truy cập, hoặc khi có sự cố xảy ra. Giao diện này có bốn phương thức tương ứng với các tình huống này:

  • preVisitDirectory - Được gọi trước các mục của một thư mục được truy cập.

  • postVisitDirectory - Được gọi sau khi tất cả các mục trong một thư mục được truy cập. Nếu gặp phải bất kỳ lỗi nào thì các ngoại lệ cụ thể được truyền cho phương thức.

  • visitFile - Được gọi trên các tập tin được truy cập. BasicFileAttributes của tập tin được truyền cho phương thức, hoặc bạn có thể sử dụng gói các thuộc tính tập tin để đọc một tập hợp cụ thể của các thuộc tính. Ví dụ, bạn có thể chọn để đọc các tập tin của DosFileAttributeView để xác định xem các tập tin có phần "ẩn" tập bit hay không.

  • visitFileFailed - Được gọi khi các tập tin không thể được truy cập. Khi đó các ngoại lệ cụ thể được truyền cho phương thức. Bạn có thể chọn để ném ngoại lệ, in nó tới console hoặc một file log.

Nếu bạn không cần phải thực hiện tất cả bốn phương thức FileVisitor, thay vì thực thi giao diện FileVisitor, bạn có thể mở rộng lớp SimpleFileVisitor. Lớp này thực thi giao diện FileVisitor, nó sẽ thăm tất cả các file trong một cây và ném ngoại lệ IOError khi gặp lỗi. Bạn có thể mở rộng lớp này và ghi đè chỉ khi nào các phương thức được bạn yêu cầu.

Dưới đây là một ví dụ dẫn xuất từ SimpleFileVisitor để in tất cả các thư mục trong một cây tập tin. Nó in các mục đầu vào là một tập tin chính quy, một liên kết tượng trưng, ​​một thư mục, hoặc một số tập tin khác mà "không xác định". Nó cũng in kích thước tính theo byte của mỗi tập tin. Bất kỳ trường hợp ngoại lệ nào khi gặp phải cũng sẽ được in ra console.

Các phương thức FileVisitor được in đậm:

import static java.nio.file.FileVisitResult.*;

public static class PrintFiles
    extends SimpleFileVisitor<Path> {

    // Print information about
    // each type of file.
    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
        if (attr.isSymbolicLink()) {
            System.out.format("Symbolic link: %s ", file);
        } else if (attr.isRegularFile()) {
            System.out.format("Regular file: %s ", file);
        } else {
            System.out.format("Other: %s ", file);
        }
        System.out.println("(" + attr.size() + "bytes)");
        return CONTINUE;
    }

    // Print each directory visited.
    @Override
    public FileVisitResult postVisitDirectory(Path dir,
                                          IOException exc) {
        System.out.format("Directory: %s%n", dir);
        return CONTINUE;
    }

    // If there is some error accessing
    // the file, let the user know.
    //