CodeGym /Blog Java /Ngẫu nhiên /Sự khác biệt giữa các lớp trừu tượng và giao diện

Sự khác biệt giữa các lớp trừu tượng và giao diện

Xuất bản trong nhóm
CHÀO! Trong bài học này, chúng ta sẽ nói về các lớp trừu tượng khác với các giao diện như thế nào và xem xét một số ví dụ với các lớp trừu tượng phổ biến. Sự khác biệt giữa lớp trừu tượng và giao diện - 1Chúng tôi đã dành một bài học riêng về sự khác biệt giữa một lớp trừu tượng và một giao diện, bởi vì chủ đề này rất quan trọng. Bạn sẽ được hỏi về sự khác biệt giữa các khái niệm này trong 90% các cuộc phỏng vấn trong tương lai. Điều đó có nghĩa là bạn nên chắc chắn tìm ra những gì bạn đang đọc. Và nếu bạn không hiểu đầy đủ điều gì đó, hãy đọc các nguồn bổ sung. Vì vậy, chúng ta biết lớp trừu tượng là gì và giao diện là gì. Bây giờ chúng ta sẽ đi qua sự khác biệt của họ.
  1. Một giao diện chỉ mô tả hành vi. Nó không có trạng thái. Nhưng một lớp trừu tượng bao gồm trạng thái: nó mô tả cả hai.

    Ví dụ, lấy Birdlớp trừu tượng và CanFlygiao diện:

    
    public abstract class Bird {
       private String species;
       private int age;
    
       public abstract void fly();
    
       public String getSpecies() {
           return species;
       }
    
       public void setSpecies(String species) {
           this.species = species;
       }
    
       public int getAge() {
           return age;
       }
    
       public void setAge(int age) {
           this.age = age;
       }
    }
    

    Hãy tạo một MockingJaylớp chim và làm cho nó kế thừa Bird:

    
    public class MockingJay extends Bird {
    
       @Override
       public void fly() {
           System.out.println("Fly, bird!");
       }
    
       public static void main(String[] args) {
    
           MockingJay someBird = new MockingJay();
           someBird.setAge(19);
           System.out.println(someBird.getAge());
       }
    }
    

    Như bạn có thể thấy, chúng ta có thể dễ dàng truy cập trạng thái của lớp trừu tượng — của nó speciesagecác biến.

    Nhưng nếu chúng ta cố gắng làm điều tương tự với một giao diện, bức tranh sẽ khác. Chúng ta có thể thử thêm các biến vào nó:

    
    public interface CanFly {
    
       String species = new String();
       int age = 10;
    
       public void fly();
    }
    
    public interface CanFly {
    
       private String species = new String(); // Error
       private int age = 10; // Another error
    
       public void fly();
    }
    

    Chúng tôi thậm chí không thể khai báo các biến riêng tư bên trong một giao diện. Tại sao? Bởi vì công cụ sửa đổi riêng được tạo để ẩn việc triển khai khỏi người dùng. Và một giao diện không có triển khai bên trong nó: không có gì để che giấu.

    Một giao diện chỉ mô tả hành vi. Theo đó, chúng tôi không thể triển khai getters và setters bên trong một giao diện. Đây là bản chất của các giao diện: chúng cần thiết để hoạt động với hành vi chứ không phải trạng thái.

    Java 8 đã giới thiệu các phương thức mặc định cho các giao diện có triển khai. Bạn đã biết về chúng, vì vậy chúng tôi sẽ không lặp lại chính mình.

  2. Một lớp trừu tượng kết nối và hợp nhất các lớp có liên quan chặt chẽ với nhau. Đồng thời, một giao diện duy nhất có thể được thực hiện bởi các lớp hoàn toàn không có gì chung.

    Hãy trở lại ví dụ của chúng tôi với các loài chim.

    Lớp trừu tượng của chúng tôi Birdlà cần thiết để tạo ra những con chim dựa trên lớp đó. Chỉ là chim và không có gì khác! Tất nhiên, sẽ có nhiều loại chim khác nhau.

    Sự khác biệt giữa lớp trừu tượng và giao diện - 2

    Với CanFlygiao diện, mọi người tiếp tục theo cách riêng của họ. Nó chỉ mô tả hành vi (bay) gắn liền với tên gọi của nó. Nhiều thứ không liên quan 'có thể bay'.

    Sự khác biệt giữa lớp trừu tượng và giao diện - 3

    4 thực thể này không liên quan đến nhau. Họ thậm chí không phải là tất cả sống. Tuy nhiên, tất cả họ CanFly.

    Chúng tôi không thể mô tả chúng bằng cách sử dụng một lớp trừu tượng. Chúng không chia sẻ cùng một trạng thái hoặc các trường giống hệt nhau. Để xác định một chiếc máy bay, có lẽ chúng ta sẽ cần các trường cho kiểu máy, năm sản xuất và số lượng hành khách tối đa. Đối với Carlson, chúng tôi sẽ cần những cánh đồng cho tất cả đồ ngọt mà cậu ấy đã ăn hôm nay và một danh sách các trò chơi mà cậu ấy sẽ chơi với em trai mình. Đối với một con muỗi, ...uh... Tôi thậm chí không biết... Có lẽ, một 'mức độ khó chịu'? :)

    Vấn đề là chúng ta không thể sử dụng một lớp trừu tượng để mô tả chúng. Họ quá khác nhau. Nhưng chúng có chung hành vi: chúng có thể bay. Một giao diện hoàn hảo để mô tả mọi thứ trên thế giới có thể bay, bơi, nhảy hoặc thể hiện một số hành vi khác.

  3. Các lớp có thể triển khai bao nhiêu giao diện tùy thích, nhưng chúng chỉ có thể kế thừa một lớp.

    Chúng tôi đã đề cập đến điều này hơn một lần. Java không có đa kế thừa các lớp, nhưng nó hỗ trợ đa kế thừa các giao diện. Điểm này nối tiếp một phần điểm trước đó: một giao diện kết nối nhiều lớp khác nhau mà thường không có điểm chung nào khác, trong khi một lớp trừu tượng được tạo cho một nhóm các lớp có liên quan rất chặt chẽ. Do đó, điều hợp lý là bạn chỉ có thể kế thừa một lớp như vậy. Một lớp trừu tượng mô tả mối quan hệ 'is-a'.

Giao diện tiêu chuẩn: InputStream và OutputStream

Chúng ta đã xem xét các lớp khác nhau chịu trách nhiệm về các luồng đầu vào và đầu ra. Hãy xem xét InputStreamOutputStream. Nói chung, đây hoàn toàn không phải là các giao diện, mà là các lớp trừu tượng hoàn toàn chính hãng. Bây giờ bạn đã biết điều đó có nghĩa là gì, vì vậy sẽ dễ dàng hơn nhiều khi làm việc với chúng :) InputStreamlà một lớp trừu tượng chịu trách nhiệm nhập byte. Java có một số lớp kế thừa InputStream. Mỗi người trong số họ được thiết kế để nhận dữ liệu từ các nguồn khác nhau. Bởi vì InputStreamlà cha mẹ, nó cung cấp một số phương thức giúp dễ dàng làm việc với các luồng dữ liệu. Mỗi hậu duệ của InputStreamcó các phương pháp này:
  • int available()trả về số byte có sẵn để đọc;
  • close()đóng luồng đầu vào;
  • int read()trả về một đại diện số nguyên của byte có sẵn tiếp theo trong luồng. Nếu đã đến cuối luồng, -1 sẽ được trả về;
  • int read(byte[] buffer)cố gắng đọc byte vào bộ đệm và trả về số byte đã đọc. Khi đến cuối tệp, nó trả về -1;
  • int read(byte[] buffer, int byteOffset, int byteCount)ghi một phần của khối byte. Nó được sử dụng khi mảng byte có thể không được lấp đầy hoàn toàn. Khi đến cuối tệp, nó trả về -1;
  • long skip(long byteCount)bỏ qua byteCount byte trong luồng đầu vào và trả về số byte bị bỏ qua.
Tôi khuyên bạn nên nghiên cứu danh sách đầy đủ các phương pháp . Thực tế có hơn mười lớp con. Ví dụ, đây là một số:
  1. FileInputStream: loại phổ biến nhất của InputStream. Nó được sử dụng để đọc thông tin từ một tập tin;
  2. StringBufferInputStream: Một loại hữu ích khác của InputStream. Nó chuyển đổi một chuỗi thành một InputStream;
  3. BufferedInputStream: Luồng đầu vào được đệm. Nó được sử dụng thường xuyên nhất để tăng hiệu suất.
Hãy nhớ khi chúng tôi đi qua BufferedReadervà nói rằng bạn không cần phải sử dụng nó? Khi chúng ta viết:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
…bạn không cần phải sử dụng BufferedReader: An InputStreamReadercó thể thực hiện công việc. Nhưng BufferedReadercải thiện hiệu suất và cũng có thể đọc toàn bộ dòng dữ liệu thay vì các ký tự riêng lẻ. Điều tương tự cũng áp dụng cho BufferedInputStream! Lớp tích lũy dữ liệu đầu vào trong một bộ đệm đặc biệt mà không cần truy cập liên tục vào thiết bị đầu vào. Hãy xem xét một ví dụ:

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class BufferedInputExample {

   public static void main(String[] args) throws Exception {
       InputStream inputStream = null;
       BufferedInputStream buffer = null;

       try {

           inputStream = new FileInputStream("D:/Users/UserName/someFile.txt");

           buffer = new BufferedInputStream(inputStream);

           while(buffer.available()>0) {

               char c = (char)buffer.read();

                System.out.println("Character read: " + c);
           }
       } catch(Exception e) {

           e.printStackTrace();

       } finally {

           inputStream.close();
           buffer.close();
       }
   }
}
Trong ví dụ này, chúng tôi đọc dữ liệu từ một tệp nằm trên máy tính tại ' D:/Users/UserName/someFile.txt '. Chúng tôi tạo 2 đối tượng — a FileInputStreamvà a BufferedInputStream'bao bọc' nó. Sau đó, chúng tôi đọc byte từ tệp và chuyển đổi chúng thành ký tự. Và chúng tôi làm điều đó cho đến khi tập tin kết thúc. Như bạn có thể thấy, không có gì phức tạp ở đây. Bạn có thể sao chép mã này và chạy nó trên một tệp thực trên máy tính của mình :) OutputStreamLớp này là một lớp trừu tượng đại diện cho một luồng byte đầu ra. Như bạn đã biết, điều này ngược lại với InputStream. Nó không chịu trách nhiệm đọc dữ liệu từ một nơi nào đó, mà là gửi dữ liệu đến một nơi nào đó . Giống như InputStream, lớp trừu tượng này cung cấp cho tất cả các lớp con của nó một tập hợp các phương thức thuận tiện:
  • void close()đóng luồng đầu ra;
  • void flush()xóa tất cả các bộ đệm đầu ra;
  • abstract void write(int oneByte)ghi 1 byte vào luồng đầu ra;
  • void write(byte[] buffer)ghi một mảng byte vào luồng đầu ra;
  • void write(byte[] buffer, int offset, int count)ghi một phạm vi đếm byte từ một mảng, bắt đầu từ vị trí offset.
Dưới đây là một số hậu duệ của OutputStreamlớp:
  1. DataOutputStream. Luồng đầu ra bao gồm các phương thức để ghi các kiểu dữ liệu Java tiêu chuẩn.

    Một lớp rất đơn giản để viết các chuỗi và kiểu dữ liệu Java nguyên thủy. Bạn có thể sẽ hiểu đoạn mã sau ngay cả khi không có lời giải thích:

    
    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           DataOutputStream dos = new DataOutputStream(new FileOutputStream("testFile.txt"));
    
           dos.writeUTF("SomeString");
           dos.writeInt(22);
           dos.writeDouble(1.21323);
           dos.writeBoolean(true);
    
       }
    }
    

    Nó có các phương thức riêng biệt cho từng loại — writeDouble(), writeLong(), writeShort()v.v.


  2. FileOutputStream. Lớp này thực hiện cơ chế gửi dữ liệu tới một tệp trên đĩa. Nhân tiện, chúng tôi đã sử dụng nó trong ví dụ trước. Bạn có để ý không? Chúng tôi đã chuyển nó tới DataOutputStream, hoạt động như một 'trình bao bọc'.

  3. BufferedOutputStream. Một luồng đầu ra được đệm. Cũng không có gì phức tạp ở đây. Mục đích của nó tương tự như BufferedInputStream(hoặc BufferedReader). Thay vì đọc dữ liệu tuần tự thông thường, nó ghi dữ liệu bằng bộ đệm 'tích lũy' đặc biệt. Bộ đệm giúp giảm số lần truy cập phần dữ liệu chìm, do đó tăng hiệu suất.

    
    import java.io.*;
    
    public class DataOutputStreamExample {
    
         public static void main(String[] args) throws IOException {
    
               FileOutputStream outputStream = new FileOutputStream("D:/Users/Username/someFile.txt");
               BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
    
               String text = "I love Java!"; // We'll convert this string to a byte array and write it to a file
    
               byte[] buffer = text.getBytes();
    
               bufferedStream.write(buffer, 0, buffer.length);
         }
    }
    

    Một lần nữa, bạn có thể tự thử với mã này và xác minh rằng nó sẽ hoạt động trên các tệp thực trên máy tính của bạn.

Chúng ta sẽ có một bài học riêng về FileInputStream, FileOutputStreamBuffreredInputStream, vì vậy đây là đủ thông tin cho người mới làm quen lần đầu. Đó là nó! Chúng tôi hy vọng bạn hiểu sự khác biệt giữa giao diện và lớp trừu tượng và sẵn sàng trả lời bất kỳ câu hỏi nào, kể cả câu hỏi mẹo :)
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION