Khái niệm tẩy xóa trong khái quát trong Java là gì?


141

Khái niệm tẩy xóa trong khái quát trong Java là gì?

Câu trả lời:


200

Về cơ bản, đó là cách mà các thế hệ được triển khai trong Java thông qua thủ thuật trình biên dịch. Mã chung được biên dịch thực sự chỉ sử dụng java.lang.Objectbất cứ nơi nào bạn nói về T(hoặc một số tham số loại khác) - và có một số siêu dữ liệu để nói với trình biên dịch rằng nó thực sự là một loại chung.

Khi bạn biên dịch một số mã theo một kiểu hoặc phương thức chung, trình biên dịch sẽ tìm ra ý nghĩa thực sự của bạn (nghĩa là đối số kiểu Tlà gì) và xác minh tại thời điểm biên dịch rằng bạn đang làm đúng, nhưng mã được phát ra lại chỉ nói chuyện về mặt java.lang.Object- trình biên dịch tạo ra các phôi bổ sung khi cần thiết. Tại thời điểm thực hiện, a List<String> và a List<Date>hoàn toàn giống nhau; thông tin loại thêm đã bị xóa bởi trình biên dịch.

So sánh điều này với, giả sử, C #, trong đó thông tin được giữ lại tại thời điểm thực thi, cho phép mã chứa các biểu thức typeof(T)tương đương với T.class- ngoại trừ thông tin sau không hợp lệ. (Có nhiều sự khác biệt giữa khái quát .NET và khái quát Java, bạn hãy nhớ.) Loại xóa là nguồn gốc của nhiều thông báo lỗi / cảnh báo "lẻ" khi xử lý các tổng quát Java.

Các nguồn lực khác:


6
@Rogerio: Không, các đối tượng sẽ không có các loại chung khác nhau. Các lĩnh vực biết các loại, nhưng các đối tượng không.
Jon Skeet

8
@Rogerio: Hoàn toàn - rất dễ dàng để tìm ra tại thời điểm thực hiện cho dù thứ gì đó chỉ được cung cấp như Object(trong một kịch bản được gõ yếu) thực sự là một List<String>) chẳng hạn. Trong Java, điều đó không khả thi - bạn có thể tìm ra rằng đó là một ArrayList, nhưng không phải kiểu chung chung ban đầu là gì. Ví dụ, loại điều này có thể xuất hiện trong các tình huống tuần tự hóa / giải tuần tự hóa. Một ví dụ khác là nơi một container phải có khả năng xây dựng các thể hiện của loại chung của nó - bạn phải chuyển loại đó một cách riêng biệt trong Java (dưới dạng Class<T>).
Jon Skeet

6
Tôi chưa bao giờ tuyên bố nó luôn hay hầu như luôn luôn là một vấn đề - nhưng nó ít nhất một cách hợp lý thường xuyên là một vấn đề trong kinh nghiệm của tôi. Có nhiều nơi tôi buộc phải thêm một Class<T>tham số vào hàm tạo (hoặc phương thức chung) đơn giản vì Java không giữ lại thông tin đó. Nhìn vào EnumSet.allOfví dụ - đối số kiểu chung cho phương thức là đủ; tại sao tôi cũng cần chỉ định một đối số "bình thường"? Trả lời: loại tẩy. Loại điều này gây ô nhiễm một API. Không quan tâm, bạn đã sử dụng .NET genericics nhiều chưa? (còn tiếp)
Jon Skeet

5
Trước khi tôi sử dụng khái quát .NET, tôi đã thấy các tổng quát Java lúng túng theo nhiều cách khác nhau (và ký tự đại diện vẫn còn là vấn đề đau đầu, mặc dù hình thức phương sai "người gọi chỉ định" chắc chắn có lợi thế) - nhưng chỉ sau khi tôi sử dụng khái quát .NET trong một thời gian tôi đã thấy có bao nhiêu mẫu trở nên lúng túng hoặc không thể với các tổng quát Java. Lại là nghịch lý Blub. Tôi không nói rằng các thế hệ .NET cũng không có nhược điểm, btw - có nhiều mối quan hệ kiểu khác nhau không thể diễn tả, thật không may - nhưng tôi thích nó hơn so với các tổng quát Java.
Jon Skeet

5
@Rogerio: Có rất nhiều thứ bạn có thể làm với sự phản chiếu - nhưng tôi không có xu hướng thấy tôi muốn làm những việc đó gần như thường xuyên như những điều mà tôi không thể làm với các khái quát về Java. Tôi không muốn tìm ra đối số loại cho một trường gần như thường xuyên như tôi muốn tìm hiểu đối số loại của một đối tượng thực tế.
Jon Skeet

41

Cũng giống như một ghi chú bên lề, đây là một bài tập thú vị để thực sự thấy trình biên dịch đang làm gì khi thực hiện xóa - làm cho toàn bộ khái niệm dễ nắm bắt hơn một chút. Có một cờ đặc biệt mà bạn có thể chuyển trình biên dịch để xuất các tệp java đã xóa các tổng quát và các phôi. Một ví dụ:

javac -XD-printflat -d output_dir SomeFile.java

Các -printflatlà lá cờ đó được trao tắt để trình biên dịch mà tạo ra các tập tin. (Phần -XDnày là thứ javacsẽ đưa nó cho jar thực thi thực sự biên dịch chứ không chỉ javac, nhưng tôi lạc đề ...) Điều -d output_dirnày là cần thiết bởi vì trình biên dịch cần một số nơi để đặt các tệp .java mới.

Điều này, tất nhiên, không chỉ là tẩy xóa; tất cả các công cụ tự động trình biên dịch được thực hiện ở đây. Ví dụ, các hàm tạo mặc định cũng được chèn vào, các forvòng lặp kiểu foreach mới được mở rộng thành các forvòng lặp thông thường , v.v ... Thật tuyệt khi thấy những điều nhỏ nhặt đang diễn ra tự động.


29

Xóa, theo nghĩa đen có nghĩa là thông tin loại có trong mã nguồn bị xóa khỏi mã byte được biên dịch. Hãy để chúng tôi hiểu điều này với một số mã.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

Nếu bạn biên dịch mã này và sau đó dịch ngược mã với trình dịch ngược Java, bạn sẽ nhận được một cái gì đó như thế này. Lưu ý rằng mã dịch ngược không chứa dấu vết của thông tin loại có trong mã nguồn gốc.

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 

Tôi đã cố gắng sử dụng trình dịch ngược java để xem mã sau khi xóa kiểu từ tệp. Class, nhưng tệp. Class vẫn có thông tin loại. Tôi đã thử jigawotnói, nó hoạt động.
thẳng thắn

25

Để hoàn thành câu trả lời của Jon Skeet đã rất đầy đủ, bạn phải nhận ra khái niệm về kiểu xóa xuất phát từ nhu cầu tương thích với các phiên bản Java trước đó .

Ban đầu được trình bày tại EclipseCon 2007 (không còn khả dụng), tính tương thích bao gồm các điểm đó:

  • Khả năng tương thích nguồn (Rất vui khi có ...)
  • Tương thích nhị phân (Phải có!)
  • Tương thích di chuyển
    • Các chương trình hiện tại phải tiếp tục hoạt động
    • Các thư viện hiện có phải có thể sử dụng các loại chung
    • Phải có!

Câu trả lời gốc:

Vì thế:

new ArrayList<String>() => new ArrayList()

Có những đề xuất cho một sự thống nhất lớn hơn . Reify là "coi một khái niệm trừu tượng là có thật", trong đó các cấu trúc ngôn ngữ nên là các khái niệm, không chỉ là cú pháp cú pháp.

Tôi cũng nên đề cập đến checkCollectionphương thức của Java 6, trả về một khung nhìn an toàn kiểu động của bộ sưu tập đã chỉ định. Bất kỳ nỗ lực để chèn một yếu tố của loại sai sẽ dẫn đến ngay lập tức ClassCastException.

Cơ chế tổng quát trong ngôn ngữ cung cấp kiểm tra kiểu thời gian biên dịch (tĩnh), nhưng có thể đánh bại cơ chế này với các phôi không được kiểm tra .

Thông thường đây không phải là một vấn đề, vì trình biên dịch đưa ra cảnh báo về tất cả các hoạt động không được kiểm tra như vậy.

Tuy nhiên, có những lúc khi kiểm tra kiểu tĩnh không đủ, như:

  • khi một bộ sưu tập được chuyển đến thư viện của bên thứ ba và điều bắt buộc là mã thư viện không làm hỏng bộ sưu tập bằng cách chèn một phần tử sai loại.
  • một chương trình thất bại với a ClassCastException, chỉ ra rằng một phần tử được gõ không chính xác đã được đưa vào một bộ sưu tập được tham số hóa. Thật không may, ngoại lệ có thể xảy ra bất cứ lúc nào sau khi phần tử lỗi được chèn vào, do đó, nó thường cung cấp ít hoặc không có thông tin nào về nguồn gốc thực sự của vấn đề.

Cập nhật tháng 7 năm 2012, gần bốn năm sau:

Bây giờ (2012) chi tiết trong " Quy tắc tương thích di chuyển API (Kiểm tra chữ ký) "

Ngôn ngữ lập trình Java thực hiện các tổng quát bằng cách sử dụng erasure, đảm bảo rằng các phiên bản kế thừa và chung thường tạo ra các tệp lớp giống hệt nhau, ngoại trừ một số thông tin phụ trợ về các loại. Khả năng tương thích nhị phân không bị phá vỡ vì có thể thay thế tệp lớp kế thừa bằng tệp lớp chung mà không thay đổi hoặc biên dịch lại bất kỳ mã máy khách nào.

Để tạo điều kiện giao tiếp với mã kế thừa không chung chung, cũng có thể sử dụng việc xóa một loại tham số hóa làm một loại. Một loại như vậy được gọi là một loại thô ( Đặc tả ngôn ngữ Java 3 / 4.8 ). Cho phép loại thô cũng đảm bảo khả năng tương thích ngược cho mã nguồn.

Theo đó, các phiên bản sau của java.util.Iteratorlớp này đều tương thích ngược với mã nguồn và mã nguồn:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

2
Lưu ý rằng khả năng tương thích ngược có thể đạt được mà không cần xóa kiểu, nhưng không phải không có lập trình viên Java học một bộ các bộ sưu tập mới. Đó chính xác là con đường mà .NET đã đi. Nói cách khác, đây là viên đạn thứ ba, đây là viên đạn quan trọng. (Tiếp tục.)
Jon Skeet

15
Cá nhân tôi nghĩ rằng đây là một sai lầm cận thị - nó mang lại lợi thế ngắn hạn và bất lợi lâu dài.
Jon Skeet

8

Bổ sung cho câu trả lời Jon Skeet đã được bổ sung ...

Nó đã được đề cập rằng việc thực hiện thuốc generic thông qua việc tẩy xóa dẫn đến một số hạn chế gây phiền nhiễu (ví dụ như không new T[42]). Nó cũng đã được đề cập rằng lý do chính để thực hiện mọi thứ theo cách này là khả năng tương thích ngược trong mã byte. Điều này cũng (hầu hết) đúng. Mã byte được tạo -target 1.5 hơi khác so với chỉ đúc không có đường -target 1.4. Về mặt kỹ thuật, thậm chí có thể (thông qua mánh khóe mênh mông) để có quyền truy cập vào các kiểu khởi tạo chung chung trong thời gian chạy , chứng minh rằng thực sự có một cái gì đó trong mã byte.

Điểm thú vị hơn (chưa được nêu ra) là việc triển khai thuốc generic sử dụng tẩy xóa mang lại sự linh hoạt hơn một chút trong những gì hệ thống loại cấp cao có thể thực hiện. Một ví dụ điển hình cho việc này sẽ là triển khai JVM của Scala so với CLR. Trên JVM, có thể trực tiếp triển khai các loại cao hơn do thực tế là chính JVM không áp dụng các hạn chế đối với các loại chung (vì các "loại" này thực sự không có). Điều này trái ngược với CLR, có kiến ​​thức về thời gian chạy tham số. Do đó, bản thân CLR phải có một số khái niệm về cách sử dụng thuốc generic, vô hiệu hóa các nỗ lực để mở rộng hệ thống với các quy tắc không dự đoán được. Kết quả là, các loại cao hơn của Scala trên CLR được triển khai bằng cách sử dụng một hình thức xóa kỳ lạ được mô phỏng trong chính trình biên dịch,

Xóa có thể bất tiện khi bạn muốn làm những việc nghịch ngợm trong thời gian chạy, nhưng nó cung cấp sự linh hoạt nhất cho các nhà văn trình biên dịch. Tôi đoán đó là một phần lý do tại sao nó sẽ không biến mất bất cứ lúc nào sớm.


6
Sự bất tiện không phải là khi bạn muốn làm những việc "nghịch ngợm" trong thời gian thực hiện. Đó là khi bạn muốn làm những điều hoàn toàn hợp lý tại thời điểm thực hiện. Trong thực tế, kiểu xóa cho phép bạn thực hiện những việc xa hơn - chẳng hạn như chuyển Danh sách <Chuỗi> sang Danh sách và sau đó vào Danh sách <Ngày> chỉ với các cảnh báo.
Jon Skeet

5

Theo tôi hiểu (là một người .NET ), JVM không có khái niệm về khái quát, vì vậy trình biên dịch thay thế các tham số kiểu bằng Object và thực hiện tất cả việc truyền cho bạn.

Điều này có nghĩa là các tổng quát Java không là gì ngoài cú pháp đường và không cung cấp bất kỳ cải tiến hiệu suất nào cho các loại giá trị yêu cầu quyền anh / bỏ hộp khi được tham chiếu.


3
Các tổng quát Java không thể biểu thị các loại giá trị - dù sao thì không có danh sách nào như Danh sách <int>. Tuy nhiên, hoàn toàn không có tham chiếu qua Java - nó hoàn toàn vượt qua giá trị (trong đó giá trị đó có thể là tham chiếu.)
Jon Skeet

2

Có những lời giải thích tốt. Tôi chỉ thêm một ví dụ để chỉ ra cách thức xóa hoạt động với trình dịch ngược.

Lớp gốc,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

Mã dịch ngược từ mã byte của nó,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}

2

Tại sao nên sử dụng Generices

Tóm lại, generic cho phép các loại (lớp và giao diện) là tham số khi xác định các lớp, giao diện và phương thức. Giống như các tham số chính thức quen thuộc hơn được sử dụng trong khai báo phương thức, các tham số loại cung cấp một cách để bạn sử dụng lại cùng một mã với các đầu vào khác nhau. Sự khác biệt là các đầu vào cho các tham số chính thức là các giá trị, trong khi các đầu vào cho các tham số loại là các loại. ode sử dụng generic có nhiều lợi ích so với mã không chung chung:

  • Kiểm tra loại mạnh hơn tại thời gian biên dịch.
  • Loại bỏ phôi.
  • Cho phép lập trình viên thực hiện các thuật toán chung.

Loại tẩy là gì

Generics đã được giới thiệu với ngôn ngữ Java để cung cấp các kiểm tra loại chặt chẽ hơn tại thời điểm biên dịch và để hỗ trợ lập trình chung. Để triển khai generic, trình biên dịch Java áp dụng kiểu xóa để:

  • Thay thế tất cả các tham số loại trong các loại chung bằng giới hạn hoặc Đối tượng của chúng nếu các tham số loại không bị ràng buộc. Do đó, mã byte được tạo ra chỉ chứa các lớp, giao diện và phương thức thông thường.
  • Chèn phôi loại nếu cần thiết để bảo vệ an toàn loại.
  • Tạo các phương thức cầu để bảo tồn tính đa hình trong các loại chung mở rộng.

[NB] -Phương pháp cầu là gì? Nói ngắn gọn, trong trường hợp giao diện được tham số hóa như Comparable<T>, điều này có thể khiến các phương thức bổ sung được chèn bởi trình biên dịch; những phương pháp bổ sung này được gọi là cầu nối.

Cách thức hoạt động

Việc xóa một loại được định nghĩa như sau: loại bỏ tất cả các tham số loại từ các loại được tham số hóa và thay thế bất kỳ biến loại nào bằng cách xóa ràng buộc của nó hoặc với Object nếu nó không có ràng buộc hoặc xóa khỏi giới hạn ngoài cùng nếu nó có nhiều giới hạn. Dưới đây là một số ví dụ:

  • Xóa bỏ hoàn của List<Integer>, List<String>List<List<String>>List.
  • Việc tẩy xóa List<Integer>[]List[].
  • Sự tẩy xóa của List là chính nó, tương tự cho bất kỳ loại thô.
  • Việc xóa int là chính nó, tương tự cho bất kỳ kiểu nguyên thủy nào.
  • Sự tẩy xóa của Integer chính nó, tương tự cho bất kỳ loại nào không có tham số loại.
  • Việc xóa Ttrong định nghĩa asListObject, bởi vìT không có ràng buộc.
  • Việc xóa Ttrong định nghĩa maxComparable, bởi vì T đã ràng buộcComparable<? super T> .
  • Việc xóa Ttrong định nghĩa cuối cùng maxObject, bởi vì Tđã bị ràng buộc Object& Comparable<T>và chúng ta thực hiện việc xóa các giới hạn ngoài cùng bên trái.

Cần cẩn thận khi sử dụng thuốc generic.

Trong Java, hai phương thức riêng biệt không thể có cùng chữ ký. Vì thuốc generic được thực hiện bằng cách xóa, nên cũng có hai phương pháp riêng biệt không thể có chữ ký có cùng độ xóa. Một lớp không thể quá tải hai phương thức có chữ ký có cùng độ xóa và một lớp không thể thực hiện hai giao diện có cùng độ xóa.

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

Chúng tôi dự định mã này sẽ hoạt động như sau:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

Tuy nhiên, trong trường hợp này, việc xóa chữ ký của cả hai phương pháp là giống hệt nhau:

boolean allZero(List)

Do đó, một cuộc đụng độ tên được báo cáo tại thời điểm biên dịch. Không thể đặt cả hai phương thức cùng tên và cố gắng phân biệt giữa chúng bằng cách nạp chồng, vì sau khi xóa, không thể phân biệt một cuộc gọi phương thức này với phương thức khác.

Hy vọng, Reader sẽ thích :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.