Đại biểu Java?


194

Ngôn ngữ Java có các tính năng dành cho đại biểu không, tương tự như cách C # hỗ trợ cho các đại biểu?


20
@Suma Làm thế nào điều này có thể là một bản sao nếu câu hỏi bạn đề cập đã được đăng một năm sau câu hỏi này?
Ani

1
Java 8 có một tính năng khá giống các đại biểu. Nó được gọi là lambdas.
tbodt

4
@tbodt để chính xác hơn, Java 8 có một tính năng khá giống các đại biểu. Nó được gọi là giao diện chức năng . Lambdas là một cách để tạo ra các thể hiện đại biểu như vậy (ẩn danh).
nawfal

3
Các câu trả lời dưới đây là từ Pre-Java 8. Đăng Java 8 xem các câu trả lời trong chuỗi này: stackoverflow.com/questions/20311779/ chủ
Michael Lloyd Lee mlk

@nawfal, +1, nhưng chính xác hơn nữa, Java 7 trở xuống đã có một tính năng khá giống các đại biểu. Nó được gọi là giao diện đơn giản.
Pacerier

Câu trả lời:


152

Không thực sự, không.

Bạn có thể đạt được hiệu ứng tương tự bằng cách sử dụng sự phản chiếu để có được các đối tượng Phương thức mà sau đó bạn có thể gọi và cách khác là tạo một giao diện với một phương thức 'gọi' hoặc 'thực thi', và sau đó khởi tạo chúng để gọi phương thức bạn quan tâm (tức là sử dụng một lớp bên trong ẩn danh).

Bạn cũng có thể thấy bài viết này thú vị / hữu ích: Một lập trình viên Java nhìn vào các đại biểu C # (@ archive.org)


3
Giải pháp với invoke () là hàm thành viên duy nhất của giao diện thực sự rất hay.
Stephane Rolland

1
Nhưng hãy xem ví dụ của tôi ở đây về những gì người ta sẽ làm, ngay cả trong Java 7, để hoàn thành tương đương với một đại biểu C #.
ToolmakerSteve

63

Tùy thuộc chính xác ý của bạn, bạn có thể đạt được hiệu ứng tương tự (chuyển qua một phương thức) bằng cách sử dụng Mẫu chiến lược.

Thay vì một dòng như thế này khai báo một chữ ký phương thức được đặt tên:

// C#
public delegate void SomeFunction();

khai báo một giao diện:

// Java
public interface ISomeBehaviour {
   void SomeFunction();
}

Để triển khai cụ thể phương thức, hãy định nghĩa một lớp thực hiện hành vi:

// Java
public class TypeABehaviour implements ISomeBehaviour {
   public void SomeFunction() {
      // TypeA behaviour
   }
}

public class TypeBBehaviour implements ISomeBehaviour {
   public void SomeFunction() {
      // TypeB behaviour
   }
}

Sau đó, bất cứ nơi nào bạn có một SomeFunctionđại biểu trong C #, hãy sử dụng một ISomeBehaviourtham chiếu thay thế:

// C#
SomeFunction doSomething = SomeMethod;
doSomething();
doSomething = SomeOtherMethod;
doSomething();

// Java
ISomeBehaviour someBehaviour = new TypeABehaviour();
someBehaviour.SomeFunction();
someBehaviour = new TypeBBehaviour();
someBehaviour.SomeFunction();

Với các lớp bên trong ẩn danh, bạn thậm chí có thể tránh khai báo các lớp được đặt tên riêng biệt và gần như coi chúng như các hàm ủy nhiệm thực sự.

// Java
public void SomeMethod(ISomeBehaviour pSomeBehaviour) {
   ...
}

...

SomeMethod(new ISomeBehaviour() { 
   @Override
   public void SomeFunction() {
      // your implementation
   }
});

Điều này có lẽ chỉ nên được sử dụng khi việc triển khai rất cụ thể đối với bối cảnh hiện tại và sẽ không có lợi khi được sử dụng lại.

Và dĩ nhiên trong Java 8, chúng thực sự trở thành các biểu thức lambda:

// Java 8
SomeMethod(() -> { /* your implementation */ });

6
+1. đây là một cách giải quyết tốt và tính dài dòng có thể được bù đắp bằng khả năng duy trì trong tương lai.
nawfal

Điều này thật tuyệt ... Hiện đang làm việc trong một dự án mà tôi không thể sử dụng sự phản chiếu do các hạn chế của dự án và cách giải quyết này giúp công việc được hoàn thành tốt đẹp :)
Jonathan Camarena

Java có quy ước đặt tên tiền tố I cho các giao diện không? Tôi chưa từng thấy điều đó trước đây
Kyle Delaney

36

Truyện ngắn: không .

Giới thiệu

Phiên bản mới nhất của môi trường phát triển Microsoft Visual J ++ hỗ trợ cấu trúc ngôn ngữ được gọi là đại biểu hoặc tham chiếu phương thức ràng buộc . Cấu trúc này và các từ khóa mới delegatevà được multicastgiới thiệu để hỗ trợ nó, không phải là một phần của ngôn ngữ lập trình Java TM , được chỉ định bởi Đặc tả ngôn ngữ Java và được sửa đổi bởi Đặc tả lớp bên trong có trong tài liệu cho phần mềm JDKTM 1.1 .

Không chắc là ngôn ngữ lập trình Java sẽ bao gồm cấu trúc này. Sun đã cân nhắc cẩn thận việc áp dụng nó vào năm 1996, đến mức xây dựng và loại bỏ các nguyên mẫu hoạt động. Kết luận của chúng tôi là các tài liệu tham khảo phương pháp ràng buộc là không cần thiết và gây bất lợi cho ngôn ngữ. Quyết định này được đưa ra với sự tư vấn của Borland International, người có kinh nghiệm trước đây với các tham chiếu phương thức ràng buộc trong Delphi Object Pascal.

Chúng tôi tin rằng các tham chiếu phương thức bị ràng buộc là không cần thiết bởi vì một sự thay thế thiết kế khác, các lớp bên trong , cung cấp chức năng tương đương hoặc vượt trội. Cụ thể, các lớp bên trong hỗ trợ đầy đủ các yêu cầu xử lý sự kiện giao diện người dùng và đã được sử dụng để triển khai API giao diện người dùng ít nhất là toàn diện như Windows Foundation Classes.

Chúng tôi tin rằng các tham chiếu phương thức bị ràng buộc là có hại vì chúng làm mất đi tính đơn giản của ngôn ngữ lập trình Java và đặc tính hướng đối tượng phổ biến của các API. Tham chiếu phương pháp giới hạn cũng giới thiệu tính không đều trong cú pháp ngôn ngữ và quy tắc phạm vi. Cuối cùng, họ pha loãng đầu tư vào các công nghệ VM vì VM được yêu cầu xử lý các loại tham chiếu bổ sung và khác nhau một cách hiệu quả.


Như nó nói trong những gì Patrick liên kết, bạn muốn sử dụng các lớp bên trong thay thế.
SCdF

16
Bài báo tuyệt vời. Tôi YÊU định nghĩa "đơn giản" của họ: Java là một ngôn ngữ được thiết kế đơn giản như trong "Java phải đơn giản để viết trình biên dịch / VM cho" trong khi bỏ qua "Java phải đơn giản để viết / đọc bởi một người" . Giải thích rất nhiều.
Juozas Kontvainis

5
Tôi chỉ nghĩ rằng SUN đã phạm một sai lầm lớn. Họ không bị thuyết phục bởi mô hình chức năng, và đó là tất cả.
Stephane Rolland

7
@Juozas: Python được tạo ra đơn giản để viết / đọc bởi một người và nó thực hiện các chức năng / đại biểu lambda .
Stephane Rolland

5
Thay thế bằng một liên kết archive.org. Ngoài ra, điều đó thực sự ngu ngốc, Oracle.
Patrick

18

Bạn đã đọc cái này chưa :

Đại biểu là một cấu trúc hữu ích trong các hệ thống dựa trên sự kiện. Về cơ bản Đại biểu là các đối tượng mã hóa một phương thức gửi đến một đối tượng đã chỉ định. Tài liệu này cho thấy các lớp bên trong java cung cấp một giải pháp chung hơn cho các vấn đề như vậy.

Đại biểu là gì? Thực sự nó rất giống với một con trỏ tới hàm thành viên như được sử dụng trong C ++. Nhưng một đại biểu chứa đối tượng đích cùng với phương thức được gọi. Lý tưởng nhất sẽ là tốt đẹp để có thể nói:

obj.registerHandler (ano.methodOne);

.. và rằng phương thức phương thức sẽ được gọi trên ano khi một số sự kiện cụ thể được nhận.

Đây là những gì cấu trúc Delegate đạt được.

Các lớp bên trong Java

Người ta đã lập luận rằng Java cung cấp chức năng này thông qua các lớp bên trong ẩn danh và do đó không cần cấu trúc Đại biểu bổ sung.

obj.registerHandler(new Handler() {
        public void handleIt(Event ev) {
            methodOne(ev);
        }
      } );

Thoạt nhìn điều này có vẻ đúng nhưng đồng thời cũng gây phiền toái. Bởi vì đối với nhiều ví dụ xử lý sự kiện, tính đơn giản của cú pháp Delegates rất hấp dẫn.

Tổng xử lý

Tuy nhiên, nếu lập trình dựa trên sự kiện được sử dụng theo cách phổ biến hơn, ví dụ, như là một phần của môi trường lập trình không đồng bộ chung, sẽ có nhiều nguy cơ hơn.

Trong một tình huống chung như vậy, việc chỉ bao gồm phương thức đích và thể hiện đối tượng đích là không đủ. Nói chung, có thể có các tham số khác được yêu cầu, được xác định trong bối cảnh khi trình xử lý sự kiện được đăng ký.

Trong tình huống chung hơn này, cách tiếp cận java có thể cung cấp một giải pháp rất thanh lịch, đặc biệt khi kết hợp với việc sử dụng các biến cuối cùng:

void processState(final T1 p1, final T2 dispatch) { 
  final int a1 = someCalculation();

  m_obj.registerHandler(new Handler() {
    public void handleIt(Event ev) {
     dispatch.methodOne(a1, ev, p1);
    }
  } );
}

chung kết * cuối cùng * cuối cùng

Có sự chú ý của bạn?

Lưu ý rằng các biến cuối cùng có thể truy cập được từ bên trong các định nghĩa phương thức lớp ẩn danh. Hãy chắc chắn nghiên cứu mã này một cách cẩn thận để hiểu sự phân nhánh. Đây có khả năng là một kỹ thuật rất mạnh mẽ. Ví dụ, nó có thể được sử dụng để có hiệu quả tốt khi đăng ký trình xử lý trong MiniDOM và trong các tình huống chung hơn.

Ngược lại, cấu trúc Delegate không cung cấp giải pháp cho yêu cầu chung chung này và do đó nên bị từ chối như một thành ngữ mà thiết kế có thể dựa vào.



5

Không, nhưng họ có thể sử dụng proxy và phản chiếu:

  public static class TestClass {
      public String knockKnock() {
          return "who's there?";
      }
  }

  private final TestClass testInstance = new TestClass();

  @Test public void
  can_delegate_a_single_method_interface_to_an_instance() throws Exception {
      Delegator<TestClass, Callable<String>> knockKnockDelegator = Delegator.ofMethod("knockKnock")
                                                                   .of(TestClass.class)
                                                                   .to(Callable.class);
      Callable<String> callable = knockKnockDelegator.delegateTo(testInstance);
      assertThat(callable.call(), is("who's there?"));
  }

Điều thú vị về thành ngữ này là bạn có thể xác minh rằng phương thức được ủy nhiệm tồn tại và có chữ ký được yêu cầu, tại thời điểm bạn tạo đại biểu (mặc dù không phải lúc biên dịch, mặc dù có thể là trình cắm FindBugs giúp đỡ ở đây), sau đó sử dụng nó một cách an toàn để ủy quyền cho các trường hợp khác nhau.

Xem mã karg trên github để biết thêm các thử nghiệmtriển khai .


2

Tôi đã triển khai hỗ trợ gọi lại / ủy nhiệm trong Java bằng cách sử dụng sự phản chiếu. Thông tin chi tiết và nguồn làm việc có sẵn trên trang web của tôi .

Làm thế nào nó hoạt động

Có một lớp nguyên tắc có tên Callback với một lớp lồng nhau có tên WithParms. API cần gọi lại sẽ lấy đối tượng Gọi lại làm tham số và, nếu cần, hãy tạo Callback.WithParms làm biến phương thức. Vì rất nhiều ứng dụng của đối tượng này sẽ được đệ quy, nên nó hoạt động rất sạch sẽ.

Với hiệu năng vẫn là ưu tiên cao đối với tôi, tôi không muốn được yêu cầu tạo một mảng đối tượng vứt đi để giữ các tham số cho mỗi lần gọi - sau tất cả trong một cấu trúc dữ liệu lớn có thể có hàng ngàn phần tử và trong quá trình xử lý tin nhắn kịch bản cuối cùng chúng ta có thể xử lý hàng ngàn cấu trúc dữ liệu.

Để đảm bảo an toàn cho chủ đề, mảng tham số cần tồn tại duy nhất cho mỗi lần gọi phương thức API và để sử dụng hiệu quả, cùng một cách sử dụng cho mỗi lần gọi lại; Tôi cần một đối tượng thứ hai sẽ rẻ để tạo ra để liên kết cuộc gọi lại với một mảng tham số để gọi. Nhưng, trong một số tình huống, invoker sẽ có một mảng tham số vì những lý do khác. Vì hai lý do này, mảng tham số không thuộc về đối tượng Gọi lại. Ngoài ra, sự lựa chọn của việc gọi (truyền các tham số dưới dạng một mảng hoặc dưới dạng các đối tượng riêng lẻ) thuộc về API bằng cách sử dụng hàm gọi lại cho phép nó sử dụng bất kỳ lời gọi nào phù hợp nhất với hoạt động bên trong của nó.

Sau đó, lớp lồng nhau WithParms là tùy chọn và phục vụ hai mục đích, nó chứa mảng đối tượng tham số cần thiết cho các yêu cầu gọi lại và nó cung cấp 10 phương thức invoke () bị quá tải (với từ 1 đến 10 tham số) để tải mảng tham số và sau đó tải mảng tham số gọi mục tiêu gọi lại.

Dưới đây là một ví dụ sử dụng hàm gọi lại để xử lý các tệp trong cây thư mục. Đây là một vượt qua xác thực ban đầu, chỉ cần đếm các tệp để xử lý và đảm bảo không vượt quá kích thước tối đa được xác định trước. Trong trường hợp này, chúng tôi chỉ tạo dòng gọi lại với lời gọi API. Tuy nhiên, chúng tôi phản ánh phương thức đích dưới dạng giá trị tĩnh để việc phản chiếu không được thực hiện mỗi lần.

static private final Method             COUNT =Callback.getMethod(Xxx.class,"callback_count",true,File.class,File.class);

...

IoUtil.processDirectory(root,new Callback(this,COUNT),selector);

...

private void callback_count(File dir, File fil) {
    if(fil!=null) {                                                                             // file is null for processing a directory
        fileTotal++;
        if(fil.length()>fileSizeLimit) {
            throw new Abort("Failed","File size exceeds maximum of "+TextUtil.formatNumber(fileSizeLimit)+" bytes: "+fil);
            }
        }
    progress("Counting",dir,fileTotal);
    }

IoUtil. ProcessDirectory ():

/**
 * Process a directory using callbacks.  To interrupt, the callback must throw an (unchecked) exception.
 * Subdirectories are processed only if the selector is null or selects the directories, and are done
 * after the files in any given directory.  When the callback is invoked for a directory, the file
 * argument is null;
 * <p>
 * The callback signature is:
 * <pre>    void callback(File dir, File ent);</pre>
 * <p>
 * @return          The number of files processed.
 */
static public int processDirectory(File dir, Callback cbk, FileSelector sel) {
    return _processDirectory(dir,new Callback.WithParms(cbk,2),sel);
    }

static private int _processDirectory(File dir, Callback.WithParms cbk, FileSelector sel) {
    int                                 cnt=0;

    if(!dir.isDirectory()) {
        if(sel==null || sel.accept(dir)) { cbk.invoke(dir.getParent(),dir); cnt++; }
        }
    else {
        cbk.invoke(dir,(Object[])null);

        File[] lst=(sel==null ? dir.listFiles() : dir.listFiles(sel));
        if(lst!=null) {
            for(int xa=0; xa<lst.length; xa++) {
                File ent=lst[xa];
                if(!ent.isDirectory()) {
                    cbk.invoke(dir,ent);
                    lst[xa]=null;
                    cnt++;
                    }
                }
            for(int xa=0; xa<lst.length; xa++) {
                File ent=lst[xa];
                if(ent!=null) { cnt+=_processDirectory(ent,cbk,sel); }
                }
            }
        }
    return cnt;
    }

Ví dụ này minh họa vẻ đẹp của phương pháp này - logic cụ thể của ứng dụng được trừu tượng hóa trong cuộc gọi lại, và sự vất vả của việc đi bộ một cây thư mục được giấu một cách độc đáo trong một phương thức tiện ích tĩnh hoàn toàn có thể tái sử dụng. Và chúng tôi không phải trả nhiều lần giá xác định và triển khai giao diện cho mỗi lần sử dụng mới. Tất nhiên, lập luận cho một giao diện là nó rõ ràng hơn nhiều về những gì cần thực hiện (nó được thi hành, không chỉ đơn giản là tài liệu) - nhưng trong thực tế tôi không thấy đó là một vấn đề để có định nghĩa gọi lại đúng.

Xác định và triển khai một giao diện không thực sự quá tệ (trừ khi bạn đang phân phối các applet, như tôi, trong đó việc tránh tạo thêm các lớp thực sự quan trọng), nhưng điều này thực sự tỏa sáng là khi bạn có nhiều cuộc gọi lại trong một lớp. Không chỉ bị buộc phải đẩy chúng vào một lớp bên trong riêng biệt được thêm vào trong ứng dụng đã triển khai, nhưng nó hoàn toàn tẻ nhạt khi lập trình và tất cả các mã của nồi hơi thực sự chỉ là "tiếng ồn".


1

Có & Không, nhưng mẫu đại biểu trong Java có thể được nghĩ theo cách này. Hướng dẫn bằng video này là về trao đổi dữ liệu giữa các hoạt động - các đoạn và nó có bản chất tuyệt vời của mẫu sắp xếp đại biểu sử dụng các giao diện.

Giao diện Java


1

Nó không có delegatetừ khóa rõ ràng là C #, nhưng bạn có thể đạt được tương tự trong Java 8 bằng cách sử dụng giao diện chức năng (tức là bất kỳ giao diện nào có chính xác một phương thức) và lambda:

private interface SingleFunc {
    void printMe();
}

public static void main(String[] args) {
    SingleFunc sf = () -> {
        System.out.println("Hello, I am a simple single func.");
    };
    SingleFunc sfComplex = () -> {
        System.out.println("Hello, I am a COMPLEX single func.");
    };
    delegate(sf);
    delegate(sfComplex);
}

private static void delegate(SingleFunc f) {
    f.printMe();
}

Mỗi đối tượng mới của kiểu SingleFuncphải thực hiện printMe(), vì vậy sẽ an toàn khi chuyển nó sang phương thức khác (ví dụ delegate(SingleFunc)) để gọi printMe()phương thức.


0

Mặc dù nó gần như không có gì rõ ràng, nhưng bạn có thể triển khai một cái gì đó như các đại biểu C # bằng cách sử dụng Java Proxy .


2
Mặc dù liên kết này có thể trả lời câu hỏi, tốt hơn là bao gồm các phần thiết yếu của câu trả lời ở đây và cung cấp liên kết để tham khảo. Câu trả lời chỉ liên kết có thể trở nên không hợp lệ nếu trang được liên kết thay đổi.
StackFlowed

2
@StackFlowed Tách liên kết, và bạn nhận được gì? Một bài viết với thông tin. Bỏ phiếu để giữ.
Scimonster

1
@Scimonster điều gì sẽ xảy ra nếu liên kết không còn hiệu lực nữa?
StackFlowed

@StackFlowed Nó vẫn cung cấp câu trả lời - sử dụng Proxy Java.
Scimonster

Sau đó, nó có thể được để lại như là một bình luận?
StackFlowed

0

Không, nhưng nó có hành vi tương tự, trong nội bộ.

Trong các đại biểu C # được sử dụng để tạo một điểm nhập riêng và chúng hoạt động giống như một con trỏ hàm.

Trong java không có thứ gì là con trỏ hàm (nhìn từ trên xuống) nhưng bên trong Java cần phải làm điều tương tự để đạt được các mục tiêu này.

Ví dụ, việc tạo các luồng trong Java yêu cầu một lớp mở rộng Chủ đề hoặc triển khai Runnable, bởi vì một biến đối tượng lớp có thể được sử dụng một con trỏ vị trí bộ nhớ.



0

Mã được mô tả cung cấp nhiều lợi thế của các đại biểu C #. Các phương thức, tĩnh hoặc động, có thể được xử lý một cách thống nhất. Sự phức tạp trong các phương thức gọi thông qua sự phản chiếu được giảm bớt và mã có thể được sử dụng lại, theo nghĩa là không yêu cầu thêm các lớp trong mã người dùng. Lưu ý rằng chúng ta đang gọi một phiên bản tiện lợi thay thế của lệnh gọi, trong đó một phương thức với một tham số có thể được gọi mà không cần tạo một mảng đối tượng. Mã dưới đây:

  class Class1 {
        public void show(String s) { System.out.println(s); }
    }

    class Class2 {
        public void display(String s) { System.out.println(s); }
    }

    // allows static method as well
    class Class3 {
        public static void staticDisplay(String s) { System.out.println(s); }
    }

    public class TestDelegate  {
        public static final Class[] OUTPUT_ARGS = { String.class };
        public final Delegator DO_SHOW = new Delegator(OUTPUT_ARGS,Void.TYPE);

        public void main(String[] args)  {
            Delegate[] items = new Delegate[3];

            items[0] = DO_SHOW .build(new Class1(),"show,);
            items[1] = DO_SHOW.build (new Class2(),"display");
            items[2] = DO_SHOW.build(Class3.class, "staticDisplay");

            for(int i = 0; i < items.length; i++) {
                items[i].invoke("Hello World");
            }
        }
    }

-11

Java không có đại biểu và tự hào về điều đó :). Từ những gì tôi đọc ở đây, tôi đã tìm thấy trong bản chất 2 cách để đại biểu giả: 1. phản ánh; 2. lớp bên trong

Những phản ánh là slooooow! Lớp bên trong không bao gồm trường hợp sử dụng đơn giản nhất: hàm sort. Không muốn đi sâu vào chi tiết, nhưng về cơ bản, giải pháp với lớp bên trong là tạo một lớp bao bọc cho một mảng các số nguyên được sắp xếp theo thứ tự tăng dần và một lớp cho một mảng các số nguyên được sắp xếp theo thứ tự giảm dần.


Lớp bên trong có liên quan gì đến việc sắp xếp? Và những gì đã được sắp xếp để làm với câu hỏi?
nawfal
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.