Tại sao tôi nên sử dụng sự phản chiếu?


29

Tôi chưa quen với Java; thông qua các nghiên cứu của tôi, tôi đọc rằng sự phản chiếu được sử dụng để gọi các lớp và phương thức, và để biết phương thức nào được thực hiện hay không.

Khi nào tôi nên sử dụng sự phản chiếu, và sự khác biệt giữa việc sử dụng sự phản chiếu và khởi tạo đối tượng và phương thức gọi theo cách truyền thống là gì?



10
Hãy chia sẻ nghiên cứu của bạn trước khi đăng. Có rất nhiều tài liệu trên StackExchange (như @Jalayn đã lưu ý) và web nói chung về sự phản chiếu. Tôi đề nghị bạn đọc, ví dụ như Hướng dẫn Java về Reflection và quay lại sau đó nếu bạn có bất kỳ câu hỏi cụ thể nào.
Péter Török

1
Đã có một triệu bản sao.
DeadMG

3
Hơn một vài lập trình viên chuyên nghiệp sẽ trả lời "hiếm khi có thể, thậm chí có thể không bao giờ."
Ross Patterson

Câu trả lời:


38
  • Sự phản chiếu chậm hơn nhiều so với việc chỉ gọi các phương thức bằng tên của chúng, bởi vì nó phải kiểm tra siêu dữ liệu trong mã byte thay vì chỉ sử dụng các địa chỉ và hằng số được biên dịch trước.

  • Reflection cũng mạnh mẽ hơn: bạn có thể lấy định nghĩa của một protectedhoặc finalthành viên, loại bỏ các bảo vệ và vận dụng nó như thể nó đã được công bố có thể thay đổi! Rõ ràng điều này làm thay đổi nhiều đảm bảo mà ngôn ngữ thường tạo ra cho các chương trình của bạn và có thể rất, rất nguy hiểm.

Và điều này khá nhiều giải thích khi sử dụng nó. Thông thường, không. Nếu bạn muốn gọi một phương thức, chỉ cần gọi nó. Nếu bạn muốn biến đổi một thành viên, chỉ cần tuyên bố nó có thể thay đổi thay vì đi đằng sau lưng của trình biên dịch.

Một cách sử dụng phản xạ trong thế giới thực hữu ích là khi viết một khung công tác phải tương tác với các lớp do người dùng định nghĩa, trong đó tác giả khung không biết các thành viên (hoặc thậm chí các lớp) sẽ là gì. Sự phản chiếu cho phép họ đối phó với bất kỳ lớp nào mà không biết trước. Chẳng hạn, tôi không nghĩ có thể viết một thư viện hướng theo khía cạnh phức tạp mà không có sự phản chiếu.

Một ví dụ khác, JUnit thường sử dụng một chút phản xạ tầm thường: nó liệt kê tất cả các phương thức trong lớp của bạn, giả định rằng tất cả những testXXXphương thức được gọi là phương thức thử nghiệm và chỉ thực hiện những phương thức đó. Nhưng điều này bây giờ có thể được thực hiện tốt hơn với các chú thích thay vào đó, và trên thực tế, JUnit 4 phần lớn đã chuyển sang chú thích thay thế.


6
"Mạnh mẽ hơn" cần được chăm sóc. Bạn không cần sự phản chiếu để có được sự hoàn chỉnh của Turing, vì vậy không có sự tính toán nào cần sự phản chiếu. Tất nhiên Turing hoàn toàn không nói gì về các loại sức mạnh khác, như khả năng I / O và, tất nhiên, sự phản chiếu.
Steve314

1
Sự phản chiếu không nhất thiết là "chậm hơn nhiều". Bạn có thể sử dụng sự phản chiếu một lần để tạo mã byte gọi trình bao bọc trực tiếp.
SK-logic

2
Bạn sẽ biết nó khi bạn cần nó. Tôi thường tự hỏi tại sao (bên ngoài thế hệ ngôn ngữ) người ta sẽ cần nó. Sau đó, đột nhiên, tôi đã ... Phải đi lên và xuống chuỗi cha mẹ / con để nhìn trộm / chọc dữ liệu khi tôi nhận được các bảng từ các nhà phát triển khác để vào một trong những hệ thống tôi duy trì.
Brian Knoblauch

@ SK-logic: thực sự để tạo mã byte, bạn hoàn toàn không cần sự phản chiếu (thực sự sự phản chiếu hoàn toàn không có API cho thao tác mã byte!).
Joachim Sauer

1
@JoachimSauer, tất nhiên, nhưng bạn sẽ cần API phản chiếu để tải mã byte được tạo này.
SK-logic

15

Tôi cũng giống như bạn một lần, tôi không biết nhiều về sự phản chiếu - vẫn không - nhưng tôi đã sử dụng nó một lần.

Tôi đã có một lớp học với hai lớp bên trong và mỗi lớp có rất nhiều phương thức.

Tôi cần phải gọi tất cả các phương thức trong lớp bên trong và gọi chúng theo cách thủ công sẽ là quá nhiều công việc.

Sử dụng sự phản chiếu, tôi có thể gọi tất cả các phương thức này chỉ trong 2-3 dòng mã, thay vì số lượng các phương thức.


4
Tại sao các downvote?
Mahmoud Hossam

1
Nâng cao cho lời mở đầu
Dmitry Minkovsky 29/07/2015

1
@MahmoudHossam Có lẽ không phải là cách thực hành tốt nhất, nhưng câu trả lời của bạn minh họa một chiến thuật quan trọng mà người ta có thể triển khai.
ankush981 7/11/2015

13

Tôi muốn nhóm sử dụng sự phản chiếu thành ba nhóm:

  1. Khởi tạo các lớp tùy ý. Ví dụ, trong khung tiêm phụ thuộc, bạn có thể khai báo rằng giao diện ThingDoer được lớp NetworkT BreathDoer triển khai. Khung công tác sau đó sẽ tìm ra hàm tạo của NetworkT BreathDoer và khởi tạo nó.
  2. Marshalling và không sắp xếp theo một số định dạng khác. Ví dụ, ánh xạ một đối tượng với getters và cài đặt tuân theo quy ước bean thành JSON và quay lại. Mã không thực sự biết tên của các trường hoặc các phương thức, nó chỉ kiểm tra lớp.
  3. Bao bọc một lớp trong một lớp chuyển hướng (có lẽ Danh sách đó không thực sự được tải, nhưng chỉ là một con trỏ tới thứ gì đó biết cách lấy nó từ cơ sở dữ liệu) hoặc giả mạo một lớp hoàn toàn (jMock sẽ tạo ra một lớp tổng hợp thực hiện giao diện cho mục đích thử nghiệm).

Đây là lời giải thích tốt nhất về sự phản chiếu mà tôi đã tìm thấy trên StackExchange. Hầu hết các câu trả lời lặp lại những gì Java Trail nói (đó là "bạn có thể truy cập tất cả các thuộc tính này", nhưng không phải là lý do tại sao), cung cấp các ví dụ về việc thực hiện với sự phản ánh dễ thực hiện hơn mà không đưa ra câu trả lời mơ hồ về cách Spring sử dụng nó Câu trả lời này thực sự đưa ra ba ví dụ hợp lệ mà CANNOT có thể dễ dàng xử lý xung quanh bởi JVM mà không cần phản ánh. Cảm ơn bạn!
ndm13

3

Sự phản chiếu cho phép một chương trình hoạt động với mã có thể không có mặt và làm như vậy một cách đáng tin cậy.

"Mã thông thường" có các đoạn mã giống như URLConnection c = nullsự hiện diện tuyệt đối của nó khiến trình nạp lớp tải lớp URLConnection như một phần của việc tải lớp này, ném ngoại lệ ClassNotFound và thoát.

Reflection cho phép bạn tải các lớp dựa trên tên của chúng ở dạng chuỗi và kiểm tra chúng cho các thuộc tính khác nhau (hữu ích cho nhiều phiên bản ngoài tầm kiểm soát của bạn) trước khi khởi chạy các lớp thực tế phụ thuộc vào chúng. Một ví dụ điển hình là mã cụ thể của OS X được sử dụng để làm cho các chương trình Java trông tự nhiên trong OS X, vốn không có trên các nền tảng khác.


2

Về cơ bản, sự phản ánh có nghĩa là sử dụng mã chương trình của bạn làm dữ liệu.

Do đó, sử dụng sự phản chiếu có thể là một ý tưởng tốt khi mã chương trình của bạn là một nguồn dữ liệu hữu ích. (Nhưng có sự đánh đổi, vì vậy nó có thể không phải lúc nào cũng là một ý tưởng hay.)

Ví dụ, hãy xem xét một lớp đơn giản:

public class Foo {
  public int value;
  public string anotherValue;
}

và bạn muốn tạo XML từ nó. Bạn có thể viết mã để tạo XML:

public XmlNode generateXml(Foo foo) {
  XmlElement root = new XmlElement("Foo");
  XmlElement valueElement = new XmlElement("value");
  valueElement.add(new XmlText(Integer.toString(foo.value)));
  root.add(valueElement);
  XmlElement anotherValueElement = new XmlElement("anotherValue");
  anotherValueElement.add(new XmlText(foo.anotherValue));
  root.add(anotherValueElement);
  return root;
}

Nhưng đây là rất nhiều mã soạn sẵn và mỗi khi bạn thay đổi lớp, bạn phải cập nhật mã. Thực sự, bạn có thể mô tả những gì mã này làm

  • tạo một phần tử XML với tên của lớp
  • cho mỗi tài sản của lớp
    • tạo một phần tử XML với tên của thuộc tính
    • đặt giá trị của thuộc tính vào phần tử XML
    • thêm phần tử XML vào thư mục gốc

Đây là một thuật toán và đầu vào của thuật toán là lớp: chúng ta cần tên của nó, và tên, loại và giá trị của các thuộc tính của nó. Đây là nơi phản ánh đến: nó cho phép bạn truy cập vào thông tin này. Java cho phép bạn kiểm tra các loại bằng cách sử dụng các phương thức của Classlớp.

Một số trường hợp sử dụng khác:

  • xác định URL trong máy chủ web dựa trên tên phương thức của lớp và tham số URL dựa trên đối số phương thức
  • chuyển đổi cấu trúc của một lớp thành một định nghĩa kiểu GraphQL
  • gọi mọi phương thức của một lớp có tên bắt đầu bằng "test" là trường hợp kiểm thử đơn vị

Tuy nhiên, phản ánh đầy đủ có nghĩa là không chỉ nhìn vào mã hiện có (mà bản thân nó được gọi là "introspection"), mà còn sửa đổi hoặc tạo mã. Có hai trường hợp sử dụng nổi bật trong Java cho việc này: proxy và giả.

Giả sử bạn có một giao diện:

public interface Froobnicator {
  void froobnicateFruits(List<Fruit> fruits);
  void froobnicateFuel(Fuel fuel);
  // lots of other things to froobnicate
}

và bạn có một triển khai thực hiện một điều thú vị:

public class PowerFroobnicator implements Froobnicator {
  // awesome implementations
}

Và trên thực tế, bạn cũng có một triển khai thứ hai:

public class EnergySaverFroobnicator implements Froobnicator {
  // efficient implementations
}

Bây giờ bạn cũng muốn một số đầu ra đăng nhập; bạn chỉ muốn một thông điệp tường trình bất cứ khi nào một phương thức được gọi. Bạn có thể thêm đầu ra nhật ký vào mọi phương thức một cách rõ ràng, nhưng điều đó sẽ gây khó chịu và bạn phải thực hiện hai lần; một lần cho mỗi lần thực hiện. (Vì vậy, thậm chí nhiều hơn khi bạn thêm nhiều triển khai.)

Thay vào đó, bạn có thể viết proxy:

public class LoggingFroobnicator implements Froobnicator {
  private Logger logger;
  private Froobnicator inner;

  // constructor that sets those two

  public void froobnicateFruits(List<Fruit> fruits) {
    logger.logDebug("froobnicateFruits called");
    inner.froobnicateFruits(fruits);
  }

  public void froobnicateFuel(Fuel fuel) {
    logger.logDebug("froobnicateFuel( called");
    inner.froobnicateFuel(fuel);
  }
  // lots of other things to froobnicate
}

Tuy nhiên, một lần nữa, có một mẫu lặp đi lặp lại có thể được mô tả bằng thuật toán:

  • proxy logger là một lớp thực hiện một giao diện
  • nó có một hàm tạo để thực hiện một giao diện khác và một logger
  • cho mọi phương thức trong giao diện
    • việc thực hiện ghi lại một thông báo "$ methodname được gọi"
    • và sau đó gọi cùng một phương thức trên giao diện bên trong, chuyển qua tất cả các đối số

và đầu vào của thuật toán này là định nghĩa giao diện.

Reflection cho phép bạn xác định một lớp mới bằng thuật toán này. Java cho phép bạn thực hiện điều này bằng cách sử dụng các phương thức của java.lang.reflect.Proxylớp và có những thư viện cung cấp cho bạn nhiều sức mạnh hơn nữa.

Vậy nhược điểm của sự phản ánh là gì?

  • Mã của bạn trở nên khó hiểu hơn. Bạn là một mức độ trừu tượng được loại bỏ thêm khỏi các hiệu ứng cụ thể của mã của bạn.
  • Mã của bạn trở nên khó gỡ lỗi hơn. Đặc biệt với các thư viện tạo mã, mã được thực thi có thể không phải là mã bạn đã viết, nhưng mã bạn đã tạo và trình gỡ lỗi có thể không hiển thị cho bạn mã đó (hoặc cho phép bạn đặt các điểm dừng).
  • Mã của bạn trở nên chậm hơn. Tự động đọc thông tin loại và truy cập các trường bằng cách xử lý thời gian chạy thay vì truy cập mã hóa chậm hơn. Tạo mã động có thể giảm thiểu hiệu ứng này, với chi phí thậm chí còn khó hơn để gỡ lỗi.
  • Mã của bạn có thể trở nên dễ vỡ hơn. Trình truy cập phản xạ động không được kiểm tra kiểu bởi trình biên dịch, nhưng ném lỗi khi chạy.

1

Sự phản chiếu có thể tự động giữ các phần của chương trình của bạn được đồng bộ hóa, trong đó trước đây, bạn sẽ phải cập nhật thủ công chương trình của mình để sử dụng các giao diện mới.


5
Cái giá bạn phải trả trong trường hợp này là bạn mất kiểm tra kiểu bởi trình biên dịch và bộ tái cấu trúc an toàn trong IDE. Đó là một sự đánh đổi mà tôi không sẵn sàng thực hiện.
Barend
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.