2011-01-26 43 views
8

Tôi quyết định viết một số hàm bậc cao phổ biến trong Java (bản đồ, bộ lọc, giảm, v.v.) được an toàn thông qua Generics, và tôi đang gặp vấn đề với ký tự đại diện khớp một chức năng cụ thể.Generics Java - triển khai các hàm bậc cao hơn như bản đồ

Chỉ cần được hoàn tất, giao diện functor là thế này:

/** 
* The interface containing the method used to map a sequence into another. 
* @param <S> The type of the elements in the source sequence. 
* @param <R> The type of the elements in the destination sequence. 
*/ 
public interface Transformation<S, R> { 

    /** 
    * The method that will be used in map. 
    * @param sourceObject An element from the source sequence. 
    * @return The element in the destination sequence. 
    */ 
    public R apply(S sourceObject); 
} 

Chức năng gây phiền hà cũng giống như một bản đồ , nhưng thay vì chuyển một Collection nó biến đổi một Bản đồ (tại I đầu tiên nghĩ rằng nó nên được gọi là mapMap, nhưng nó có vẻ ngu ngốc đến mức tôi đã gọi nó là remapEntries).

phiên bản đầu tiên của tôi là (và tham gia một ngồi, bởi vì chữ ký là khá một con quái vật):

/** 
    * <p> 
    * Fills a map with the results of applying a mapping function to 
    * a source map. 
    * </p> 
    * Considerations: 
    * <ul> 
    * <li>The result map must be non-null, and it's the same object what is returned 
    * (to allow passing an unnamed new Map as argument).</li> 
    * <li>If the result map already contained some elements, those won't 
    * be cleared first.</li> 
    * <li>If various elements have the same key, only the last entry given the 
    * source iteration order will be present in the resulting map (it will 
    * overwrite the previous ones).</li> 
    * </ul> 
    * 
    * @param <SK> Type of the source keys. 
    * @param <SV> Type of the source values. 
    * @param <RK> Type of the result keys. 
    * @param <RV> Type of the result values. 
    * @param <MapRes> 
    * @param f The object that will be used to remapEntries. 
    * @param source The map with the source entries. 
    * @param result The map where the resulting entries will be put. 
    * @return the result map, containing the transformed entries. 
    */ 
    public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, Map.Entry<RK,RV>> f, final Map<SK, SV> source, MapRes result) { 
     for (Map.Entry<SK, SV> entry : source.entrySet()) { 
      Map.Entry<RK, RV> res = f.apply(entry); 
      result.put(res.getKey(), res.getValue()); 
     } 
     return result; 
    } 

Và nó có vẻ là khá đúng, nhưng vấn đề là việc chuyển đổi sử dụng phải phù hợp chính xác nhập các thông số, khiến việc sử dụng lại các chức năng bản đồ cho các loại tương thích trở nên khó khăn. Vì vậy, tôi quyết định thêm ký tự đại diện cho chữ ký, và nó đã kết thúc như thế này:

public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<? super Map.Entry<? super SK, ? super SV>, ? extends Map.Entry<? extends RK, ? extends RV>> f, final Map<SK, SV> source, MapRes result) { 
    for (Map.Entry<SK, SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply(entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

Nhưng khi tôi đang cố gắng để kiểm tra nó, phù hợp với ký tự đại diện thất bại:

@Test 
public void testRemapEntries() { 
    Map<String, Integer> things = new HashMap<String, Integer>(); 
    things.put("1", 1); 
    things.put("2", 2); 
    things.put("3", 3); 

    Transformation<Map.Entry<String, Number>, Map.Entry<Integer, String>> swap = new Transformation<Entry<String, Number>, Entry<Integer, String>>() { 
     public Entry<Integer, String> apply(Entry<String, Number> sourceObject) { 
      return new Pair<Integer, String>(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry 
     } 
    }; 

    Map<Integer, String> expected = new HashMap<Integer, String>(); 
    expected.put(1, "1"); 
    expected.put(2, "2"); 
    expected.put(3, "3"); 

    Map<Integer, String> result = IterUtil.remapEntries(swap, things, new HashMap<Integer, String>()); 
    assertEquals(expected, result); 
} 

Lỗi này là:

method remapEntries in class IterUtil cannot be applied to given types 
    required: Transformation<? super java.util.Map.Entry<? super SK,? super SV>,? extends java.util.Map.Entry<? extends RK,? extends RV>>,java.util.Map<SK,SV>,MapRes 
    found: Transformation<java.util.Map.Entry<java.lang.String,java.lang.Number>,java.util.Map.Entry<java.lang.Integer,java.lang.String>>,java.util.Map<java.lang.String,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.String> 

Vì vậy, bất kỳ gợi ý nào về cách sửa lỗi này? Hoặc tôi có nên từ bỏ và viết các vòng lặp rõ ràng cho việc này không?^_^

+0

Hãy xem https://github.com/GlenKPeterson/fp4java7 Các hàm bậc cao hơn của Java được thực hiện dưới dạng các transormations lười trên các bộ sưu tập không thể thay đổi (hoặc có thể thay đổi). Một số biến đổi lười biếng liên tục cũng được thực hiện. Đó là một giao diện hoàn toàn chung chung, mặc dù một vài phôi thích hợp trong việc thực hiện. – GlenPeterson

+0

hehe, bạn trễ 3 năm @GlenPeterson;) btw, thêm một số bài kiểm tra! : D – fortran

Trả lời

5

Tôi nghĩ bạn nên xem Google Guava API.

Ở đó bạn có thể tìm thấy giao diện Function tương tự như giao diện Chuyển đổi của bạn. Ngoài ra còn có một lớp Maps với các phương thức tiện ích để tạo hoặc biến đổi các bản đồ.

Bạn cũng nên xem xét PECS khi triển khai phương pháp sử dụng Generics.

+0

Tôi đã nhìn thấy những gì Guava làm, họ có một lớp chuyển đổi riêng biệt cho bản đồ ... Nó không phải là rất thanh lịch, nhưng tôi đoán đó là như xa như bạn có thể đi với những hạn chế Java Generics. – fortran

+1

+1 cho ổi. Bạn đang phát minh lại bánh xe ở đây. Ổi có các phương pháp và lớp học phù hợp với nhu cầu của bạn –

+2

@Shervin Tôi đã biết tôi đã tái phát minh ra bánh xe, nhưng đó là một bài tập tốt để thông thạo hơn với các ngữ nghĩa generics. – fortran

5

Đây là một điều khó khăn. Kiến thức sau đây hoàn toàn vô dụng và không ai quan tâm đến sở hữu:

Điều đầu tiên cần sửa là loại swap. Loại đầu vào không được là Entry<String,Number>, vì nó không thể chấp nhận Entry<String,Integer>, không phải là loại phụ của E<S,N>. Tuy nhiên, E<S,I> là một loại phụ của E<? extends S,? extends N>. Vì vậy, máy biến áp của chúng tôi nên coi đó là đầu vào. Đối với đầu ra, không có thẻ hoang dã, bởi vì biến áp chỉ có thể khởi tạo một loại bê tông anyway. Chúng tôi chỉ muốn được trung thực và chính xác về những gì có thể được tiêu thụ và những gì sẽ phải xuất trình:

/*  */ Transformation< 
        Entry<? extends String, ? extends Number>, 
        Entry<Integer, String> 
       > swap 
     = new Transformation< 
        Entry<? extends String, ? extends Number>, 
        Entry<Integer, String>>() 
    { 
     public Entry<Integer, String> apply(
      Entry<? extends String, ? extends Number> sourceObject) 
     { 
      return new Pair<Integer, String>(
       sourceObject.getValue().intValue(), 
       sourceObject.getKey() 
      ); 
     } 
    }; 

Note String là cuối cùng và không ai mở rộng nó, nhưng tôi sợ hệ thống chung mà không phải là thông minh để biết rằng, do đó, như một vấn đề về nguyên tắc, tôi đã làm ? extends String anyway, cho sau này tốt.

Sau đó, hãy suy nghĩ về remapEntries().Chúng tôi nghi ngờ rằng hầu hết các máy biến áp đi qua nó sẽ có tuyên bố loại tương tự như là swap, vì những lý do chúng tôi đặt ra. Vì vậy, chúng tôi tốt hơn có

remapEntry( 
    Transformation< 
     Entry<? extends SK, ? extends SV>, 
     Entry<RK,RV> 
     > f, 
    ... 

để đối sánh chính xác đối số đó. Từ đó, chúng tôi làm việc ra các loại nguồn và kết quả, chúng tôi muốn họ được như chung càng tốt:

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> 
RM remapEntries(
    Transformation< 
     Entry<? extends SK, ? extends SV>, 
     Entry<RK,RV> 
     > f, 
    Map<? extends SK, ? extends SV> source, 
    RM result 
) 
{ 
    for(Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Entry<RK,RV> res = f.apply(entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

RM là không cần thiết, nó là tốt để sử dụng trực tiếp Map<? super RK, ? super RV>. Nhưng có vẻ như bạn muốn loại trả lại giống với loại result trong ngữ cảnh của người gọi. Tôi sẽ chỉ đơn giản là thực hiện kiểu trả về void - đã có đủ sự cố.

Điều này sẽ không thành công, nếu swap không sử dụng ? extends. Ví dụ: nếu kiểu đầu vào là String-Integer, thật là vô lý khi thực hiện ? extends trong số đó. Nhưng bạn có thể có một phương thức nạp chồng với khai báo kiểu tham số khác nhau để khớp với trường hợp này.

Ok, điều đó đã hiệu quả, ngoài sự may mắn tuyệt đối. Nhưng, hoàn toàn là không phải là đáng giá. Cuộc sống của bạn tốt hơn nhiều nếu bạn chỉ quên nó, và sử dụng kiểu thô, ghi lại các tham số bằng tiếng Anh, làm kiểm tra kiểu khi chạy. Hãy tự hỏi, phiên bản chung có mua gì cho bạn không? Rất ít, ở mức giá khổng lồ của việc hiển thị mã của bạn hoàn toàn không thể hiểu được. Không ai, kể cả bản thân bạn và bản thân tôi, có thể hiểu được nếu chúng ta đọc chữ ký của phương thức vào sáng mai. Nó còn tệ hơn nhiều so với regex.

+0

Tôi đã đặt cược rằng ký tự đại diện sẽ bắt (' ') được sử dụng trong' f' và 'nguồn' không khớp: -/ – fortran

+0

Tôi mất một thời gian, nhưng giờ tôi nghĩ mình đã có cái nhìn sâu sắc! :-) Điều quan trọng là việc nắm bắt sẽ được thực hiện chỉ ở cấp độ loại bên ngoài, nhưng ký tự đại diện ở đây thực sự là một phần của chữ ký. – fortran

1

Điều gì đó đột nhiên xuất hiện trong đầu: nếu ký tự đại diện trong tham số chung lồng nhau sẽ không bị bắt vì chúng là một phần của loại, thì tôi có thể sử dụng giới hạn ngược trong bản đồ thay vì sử dụng chúng trong Transformation.

public static <SK, SV, RK, RV, MapRes extends Map<? super RK, ? super RV>> 
    MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, 
              Map.Entry<RK, RV>> f, 
         final Map<? extends SK, ? extends SV> source, 
         MapRes result) { 
    for (Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply((Map.Entry<SK, SV>)entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

Vấn đề duy nhất là chúng tôi phải thực hiện thao tác bỏ chọn trong Transformation.apply. Sẽ hoàn toàn an toàn nếu giao diện Map.Entrychỉ đọc, vì vậy chúng tôi chỉ có thể gạch chéo ngón tay và hy vọng rằng chuyển đổi không cố gắng gọi Map.Entry.setValue.

Chúng tôi vẫn có thể vượt qua trình bao bọc không thể thay đổi của giao diện Map.Entry đã ném ngoại lệ nếu phương pháp setValue được gọi để đảm bảo ít nhất là loại an toàn thời gian chạy.

Hoặc chỉ cần thực hiện một giao diện nhập cảnh bất biến rõ ràng và sử dụng nó, nhưng đó là một chút như gian lận (như có hai biến đổi khác nhau):

public interface ImmutableEntry<K, V> { 
    public K getKey(); 
    public V getValue(); 
} 

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> RM remapEntries(final Transformation<ImmutableEntry<SK, SV>, Map.Entry<RK, RV>> f, 
     final Map<? extends SK, ? extends SV> source, 
     RM result) { 
    for (final Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply(new ImmutableEntry<SK, SV>() { 
      public SK getKey() {return entry.getKey();} 
      public SV getValue() {return entry.getValue();} 
     }); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 
Các vấn đề liên quan