2010-10-30 14 views
7

Tôi gặp phải hành vi lạ khi cố gắng ghi đè phương thức với accessor mặc định (ví dụ: void run()). Theo đặc tả Java, một lớp có thể sử dụng hoặc ghi đè lên các thành viên mặc định của lớp cơ sở nếu các lớp thuộc cùng một gói. Mọi thứ hoạt động chính xác trong khi tất cả các lớp được nạp từ cùng một trình nạp lớp. Nhưng nếu tôi cố gắng tải một lớp con từ riêng biệt trình nạp lớp thì tính đa hình sẽ không hoạt động.Ghi đè phương thức truy cập mặc định trên các trình nạp lớp khác nhau phá vỡ đa hình

Đây là mẫu:

App.java:

import java.net.*; 
import java.lang.reflect.Method; 

public class App { 
    public static class Base { 
     void run() { 
      System.out.println("error"); 
     } 
    } 
    public static class Inside extends Base { 
     @Override 
     void run() { 
      System.out.println("ok. inside"); 
     } 
    } 
    public static void main(String[] args) throws Exception { 
     { 
      Base p = (Base) Class.forName(Inside.class.getName()).newInstance(); 
      System.out.println(p.getClass()); 
      p.run(); 
     } { 
      // path to Outside.class 
      URL[] url = { new URL("file:/home/mart/workspace6/test2/bin/") }; 
      URLClassLoader ucl = URLClassLoader.newInstance(url); 
      final Base p = (Base) ucl.loadClass("Outside").newInstance(); 
      System.out.println(p.getClass()); 
      p.run(); 
      // try reflection 
      Method m = p.getClass().getDeclaredMethod("run"); 
      m.setAccessible(true); 
      m.invoke(p); 
     } 
    } 
} 

Outside.java: phải ở trong thư mục riêng biệt. khác classloader sẽ giống nhau

public class Outside extends App.Base { 
    @Override 
    void run() { 
     System.out.println("ok. outside"); 
    } 
} 

Sản lượng:

class App$Inside 
ok. inside 
class Outside 
error 
ok. outside 

Vì vậy, sau đó tôi gọi Outside#run() tôi đã Base#run() ("lỗi" trong đầu ra). Reflections hoạt động chính xác.

Có gì không ổn? Hay là hành vi mong đợi? Tôi có thể giải quyết vấn đề này bằng cách nào đó không?

+0

Tôi nhận thấy rằng lớp 'Bên ngoài' của bạn tuyên bố' chạy' là công khai ... Tôi tự hỏi nếu đó là vấn đề. Bạn có thể thử nó với 'Outside.run' có quyền truy cập mặc định không? –

+0

Nếu 'External # run' có accessor mặc định khi Reflections (không có' setAccessible (true) ') _also_ không hoạt động:' 'Exception in thread" main "' java.lang.IllegalAccessException: Class App không thể truy cập vào thành viên class Outside with modifiers "" ' – mart

+0

Tôi viết lại mẫu cho External # run có accessor mặc định – mart

Trả lời

5

Từ Java Virtual Machine Specification:

5,3 Creation và tải
...
Tại thời gian chạy, một lớp hoặc giao diện là được xác định không chỉ bằng tên của nó, nhưng bởi một cặp: tên đầy đủ của nó và trình tải lớp xác định của nó. Mỗi lớp hoặc giao diện như vậy thuộc về gói thời gian chạy đơn .Gói thời gian hoạt động của một lớp hoặc giao diện là được xác định theo tên gói và xác định trình nạp lớp của lớp hoặc giao diện .


5.4.4 Access Control
...
Một lĩnh vực hoặc phương pháp R là truy cập để một lớp hoặc giao diện D nếu và chỉ nếu có của các điều kiện sau là đúng:

  • ...
  • R là protected hoặc gói riêng tư (nghĩa là, không phải public cũng không protected cũng không private), và là tuyên bố bởi một lớp trong cùng một gói runtime như D.
+0

Cảm ơn bạn. Bây giờ vấn đề là rõ ràng. – mart

+0

Có cách nào để thêm lớp bên ngoài vào trình nạp lớp chính không? – mart

+1

@mart: Trên thực tế, bạn có thể gọi phương thức được bảo vệ 'URLClassLoader.addURL()' qua phản ánh (với 'setAccessible (true)'), và nó hoạt động như mong muốn, nhưng nó chắc chắn là một hack. Tuy nhiên, bạn có thể viết trình nạp lớp của riêng bạn, nơi thêm tài nguyên vào thời gian chạy sẽ là hành vi hợp pháp và sử dụng nó để tải tất cả các lớp này. – axtavt

2

Đặc tả ngôn ngữ Java yêu cầu rằng một lớp chỉ có thể ghi đè lên các phương thức mà nó có thể truy cập. Nếu phương thức siêu lớp không thể truy cập được, nó sẽ bị che khuất chứ không bị ghi đè.

Phản ánh "hoạt động" vì bạn yêu cầu Outside.class cho phương pháp chạy của nó. Nếu bạn hỏi Base.class thay vào đó, bạn sẽ nhận được việc thực hiện siêu:

 Method m = Base.class.getDeclaredMethod("run"); 
     m.setAccessible(true); 
     m.invoke(p); 

Bạn có thể xác minh rằng phương pháp này được coi là không thể tiếp cận bằng cách thực hiện:

public class Outside extends Base { 
    @Override 
    public void run() { 
     System.out.println("Outside."); 
     super.run(); // throws an IllegalAccessError 
    } 
} 

Vì vậy, tại sao phương pháp này không thể truy xuất? Tôi không hoàn toàn chắc chắn, nhưng tôi nghi ngờ rằng các lớp khác nhau được nạp bởi các trình nạp lớp khác nhau dẫn đến các lớp thời gian chạy khác nhau, các gói có tên giống nhau được nạp bởi các trình nạp lớp khác nhau dẫn đến các gói thời gian chạy khác nhau.

Sửa: Thực ra, API phản chiếu nói rằng đó là cùng một gói:

Base.class.getPackage() == p.getClass().getPackage() // true 
+0

Cảm ơn bạn đã trả lời. Nghi ngờ của bạn có vẻ hợp lý. Có lẽ JVM xem xét các gói khác nhau cho các trình nạp lớp khác nhau. – mart

+0

Có phải cách để tránh điều này không? Có thể thêm một lớp vào trình nạp lớp chính? – mart

1

Tôi tìm thấy hack) chiều (để nạp lớp bên ngoài trong classloader chính vì thế vấn đề này đã biến mất.

Đọc một lớp dưới dạng byte và gọi phương thức ClassLoader#defineClass được bảo vệ.

mã:

URL[] url = { new URL("file:/home/mart/workspace6/test2/bin/") }; 
URLClassLoader ucl = URLClassLoader.newInstance(url); 

InputStream is = ucl.getResourceAsStream("Outside.class"); 
byte[] bytes = new byte[is.available()]; 
is.read(bytes); 
Method m = ClassLoader.class.getDeclaredMethod("defineClass", new Class[] { String.class, byte[].class, int.class, int.class }); 
m.setAccessible(true); 
Class<Base> outsideClass = (Class<Base>) m.invoke(Base.class.getClassLoader(), "Outside", bytes, 0, bytes.length); 

Base p = outsideClass.newInstance(); 
System.out.println(p.getClass()); 
p.run(); 

đầu ra ok. outside như mong đợi.