Trả về ImmutableMap hay Map thì tốt hơn?


90

Giả sử tôi đang viết một phương thức sẽ trả về một Bản đồ . Ví dụ:

public Map<String, Integer> foo() {
  return new HashMap<String, Integer>();
}

Sau khi suy nghĩ về nó một lúc, tôi quyết định rằng không có lý do gì để sửa đổi Bản đồ này sau khi nó được tạo. Vì vậy, tôi muốn quay trở lại một ImmutableMap .

public Map<String, Integer> foo() {
  return ImmutableMap.of();
}

Tôi nên để kiểu trả về là một Bản đồ chung hay tôi nên chỉ định rằng tôi đang trả về một Bản đồ Immutable?

Từ một phía, đây chính là lý do tại sao các giao diện được tạo ra cho; để ẩn các chi tiết triển khai.
Mặt khác, nếu tôi để nó như thế này, các nhà phát triển khác có thể bỏ lỡ thực tế rằng đối tượng này là bất biến. Vì vậy, tôi sẽ không đạt được mục tiêu chính là các đối tượng bất biến; để làm cho mã rõ ràng hơn bằng cách giảm thiểu số lượng đối tượng có thể thay đổi. Thậm chí tệ nhất, sau một thời gian, ai đó có thể cố gắng thay đổi đối tượng này và điều này sẽ dẫn đến lỗi thời gian chạy (Trình biên dịch sẽ không cảnh báo về điều đó).


2
Tôi nghi ngờ ai đó cuối cùng sẽ gắn thẻ câu hỏi này là quá mở cho các cuộc tranh luận. Bạn có thể đặt câu hỏi cụ thể hơn thay vì "WDYT?" và "có ổn không ...?" Ví dụ: "có bất kỳ vấn đề hoặc cân nhắc nào khi trả lại bản đồ thông thường không?" và "những lựa chọn thay thế nào tồn tại?"
ash

Điều gì sẽ xảy ra nếu sau này bạn quyết định rằng nó thực sự sẽ trả về một bản đồ có thể thay đổi?
dùng253751

3
@immibis lol, hay một Danh sách cho vấn đề đó?
djechlin

3
Bạn có thực sự muốn đưa sự phụ thuộc vào Guava vào API của mình không? Bạn sẽ không thể thay đổi điều đó nếu không phá vỡ khả năng tương thích ngược.
user2357112 hỗ trợ Monica

1
Tôi cho rằng câu hỏi quá sơ sài, thiếu nhiều chi tiết quan trọng. Ví dụ, dù bạn đã có Guava dưới dạng phụ thuộc (hiển thị) hay chưa. Ngay cả khi bạn đã có nó, các đoạn mã vẫn là mã pesudocode và không truyền đạt những gì bạn thực sự muốn đạt được. Bạn chắc chắn sẽ không viết return ImmutableMap.of(somethingElse). Thay vào đó, bạn sẽ lưu trữ ImmutableMap.of(somethingElse)dưới dạng một trường và chỉ trả lại trường này. Tất cả điều này ảnh hưởng đến thiết kế ở đây.
Marco 13

Câu trả lời:


70
  • Nếu bạn đang viết một API công khai và tính bất biến là một khía cạnh quan trọng trong thiết kế của bạn, tôi chắc chắn sẽ làm rõ ràng bằng cách đặt tên của phương thức biểu thị rõ ràng rằng bản đồ được trả về sẽ là bất biến hoặc bằng cách trả về kiểu cụ thể của bản đô. Đề cập đến nó trong javadoc theo ý kiến ​​của tôi là không đủ.

    Vì dường như bạn đang sử dụng triển khai Guava, tôi đã xem tài liệu và đó là một lớp trừu tượng nên nó cung cấp cho bạn một chút linh hoạt về kiểu cụ thể, thực tế.

  • Nếu bạn đang viết một công cụ / thư viện nội bộ, bạn chỉ cần trả về đơn giản là có thể chấp nhận được Map. Mọi người sẽ biết về nội dung của mã họ đang gọi hoặc ít nhất sẽ dễ dàng truy cập vào nó.

Kết luận của tôi sẽ là rõ ràng là tốt, đừng để mọi thứ theo cơ hội.


1
Một giao diện bổ sung, một lớp trừu tượng bổ sung và một lớp mở rộng lớp trừu tượng này. Đây là quá nhiều rắc rối cho một điều đơn giản như OP đang yêu cầu, đó là nếu phương thức trả về Mapkiểu giao diện hoặc ImmutableMapkiểu triển khai.
fps

20
Nếu bạn đang đề cập đến Guava's ImmutableMap, lời khuyên của nhóm Guava là hãy xem xét ImmutableMapmột giao diện có đảm bảo ngữ nghĩa và bạn nên trả lại loại đó trực tiếp.
Louis Wasserman

1
@LouisWasserman mm, không thấy câu hỏi đưa ra liên kết đến việc triển khai Guava. Sau đó, tôi sẽ xóa đoạn mã, cảm ơn bạn
Dici

3
Tôi đồng ý với Louis, nếu bạn có ý định trả về một bản đồ là một đối tượng Bất biến, thì tốt nhất hãy đảm bảo rằng đối tượng trả về thuộc loại đó. Nếu vì lý do nào đó bạn muốn che giấu sự thật rằng nó là một kiểu bất biến, hãy xác định giao diện của riêng bạn. Để nó là Bản đồ là gây hiểu lầm.
WSimpson

1
@WSimpson Tôi tin rằng đó là câu trả lời của tôi. Louis đang nói về một số đoạn trích tôi đã thêm, nhưng tôi đã xóa nó.
Dici

38

Bạn nên có ImmutableMaplàm loại trả lại của bạn. Mapchứa các phương thức không được hỗ trợ bởi việc triển khai ImmutableMap(ví dụ put) và được đánh dấu @deprecatedtrong ImmutableMap.

Việc sử dụng các phương thức không dùng nữa sẽ dẫn đến cảnh báo trình biên dịch và hầu hết các IDE sẽ cảnh báo khi mọi người cố gắng sử dụng các phương thức không dùng nữa.

Cảnh báo nâng cao này thích hợp hơn khi có ngoại lệ thời gian chạy vì gợi ý đầu tiên của bạn rằng có điều gì đó không ổn.


1
Theo tôi đây là câu trả lời hợp lý nhất. Giao diện Bản đồ là một hợp đồng và ImmutableMap rõ ràng đang vi phạm một phần quan trọng của nó. Sử dụng Bản đồ làm kiểu trả về trong trường hợp này không có ý nghĩa.
TonioElGringo

@TonioElGringo Collections.immutableMapsau đó thì sao?
OrangeDog

@TonioElGringo xin lưu ý rằng các phương thức mà ImmutableMap không triển khai được đánh dấu rõ ràng là tùy chọn trong tài liệu của giao diện docs.oracle.com/javase/7/docs/api/java/util/… . Vì vậy, tôi nghĩ rằng vẫn có thể có ý nghĩa khi trả về một Bản đồ (với javadocs thích hợp). Mặc dù vậy, tôi chọn trả lại ImmutableMap một cách rõ ràng, vì những lý do đã thảo luận ở trên.
AMT

3
@TonioElGringo nó không vi phạm bất cứ điều gì, những phương pháp đó là tùy chọn. Giống như trong List, không có đảm bảo List::addlà hợp lệ. Cố gắng Arrays.asList("a", "b").add("c");. Không có dấu hiệu nào cho thấy nó sẽ bị lỗi trong thời gian chạy, nhưng nó đã xảy ra. Ít nhất thì Ổi cũng mang lại cho bạn một sự ngạc nhiên.
njzk2

14

Mặt khác, nếu tôi để nó như thế này, các nhà phát triển khác có thể bỏ lỡ sự thật rằng đối tượng này là bất biến.

Bạn nên đề cập đến điều đó trong javadocs. Các nhà phát triển đã đọc chúng, bạn biết đấy.

Vì vậy, tôi sẽ không đạt được mục tiêu chính là các đối tượng bất biến; để làm cho mã rõ ràng hơn bằng cách giảm thiểu số lượng đối tượng có thể thay đổi. Thậm chí tệ nhất là sau một thời gian, ai đó có thể cố gắng thay đổi đối tượng này và điều này sẽ dẫn đến lỗi thời gian chạy (Trình biên dịch sẽ không cảnh báo về điều đó).

Không có nhà phát triển nào xuất bản mã của mình chưa được kiểm tra. Và khi anh ta kiểm tra nó, anh ta nhận được một Ngoại lệ ở đó anh ta không chỉ nhìn thấy lý do mà còn cả tệp và dòng nơi anh ta cố gắng ghi vào một bản đồ bất biến.

Tuy nhiên, hãy lưu ý, chỉ Mapbản thân nó sẽ là bất biến, không phải các đối tượng mà nó chứa.


10
"Không có nhà phát triển nào xuất bản mã của mình chưa được kiểm tra." Bạn có muốn đặt cược vào điều này? Sẽ có ít lỗi hơn (theo giả định của tôi) trong phần mềm công cộng, nếu câu này đúng. Tôi thậm chí không chắc "hầu hết các nhà phát triển ..." sẽ đúng. Và vâng, điều đó thật đáng thất vọng.
Tom

1
Được rồi, Tom đúng, tôi không chắc bạn đang muốn nói gì, nhưng những gì bạn thực sự viết là sai rõ ràng. (Ngoài ra: thúc đẩy sự hòa nhập giới tính)
djechlin

@ Tom Tôi đoán bạn bỏ lỡ mỉa mai trong câu trả lời này
Captain Fogetti

10

nếu tôi để nó như thế này, các nhà phát triển khác có thể bỏ lỡ thực tế rằng đối tượng này là bất biến

Điều đó đúng, nhưng các nhà phát triển khác nên kiểm tra mã của họ và đảm bảo rằng nó được bao phủ.

Tuy nhiên, bạn có 2 tùy chọn khác để giải quyết vấn đề này:

  • Sử dụng Javadoc

    @return a immutable map
  • Chọn tên phương pháp mô tả

    public Map<String, Integer> getImmutableMap()
    public Map<String, Integer> getUnmodifiableEntries()

    Đối với một trường hợp sử dụng cụ thể, bạn thậm chí có thể đặt tên cho các phương pháp tốt hơn. Ví dụ

    public Map<String, Integer> getUnmodifiableCountByWords()

Bạn còn có thể làm gì khác nữa không?!

Bạn có thể trả lại một

  • sao chép

    private Map<String, Integer> myMap;
    
    public Map<String, Integer> foo() {
      return new HashMap<String, Integer>(myMap);
    }

    Cách tiếp cận này nên được sử dụng nếu bạn mong đợi rằng nhiều khách hàng sẽ sửa đổi bản đồ và miễn là bản đồ chỉ chứa một vài mục nhập.

  • CopyOnWriteMap

    bộ sưu tập copy on write thường được sử dụng khi bạn phải xử lý
    đồng thời. Nhưng khái niệm này cũng sẽ giúp ích cho bạn trong tình huống của bạn, vì CopyOnWriteMap tạo ra một bản sao của cấu trúc dữ liệu nội bộ trên một hoạt động thay đổi (ví dụ: thêm, bớt).

    Trong trường hợp này, bạn cần một lớp bọc mỏng xung quanh bản đồ của mình để ủy quyền tất cả các lệnh gọi phương thức cho bản đồ bên dưới, ngoại trừ các thao tác gây đột biến. Nếu một thao tác đột biến được gọi, nó sẽ tạo ra một bản sao của bản đồ bên dưới và tất cả các lệnh gọi khác sẽ được ủy quyền cho bản sao này.

    Cách tiếp cận này nên được sử dụng nếu bạn mong đợi rằng một số khách hàng sẽ sửa đổi bản đồ.

    Đáng buồn là java không có như vậy CopyOnWriteMap. Nhưng bạn có thể tìm một bên thứ ba hoặc tự mình thực hiện.

Cuối cùng, bạn nên nhớ rằng các yếu tố trong bản đồ của bạn vẫn có thể thay đổi được.


8

Chắc chắn trả về ImmutableMap, biện minh là:

  • Chữ ký phương thức (bao gồm cả kiểu trả về) phải được tự lập tài liệu. Nhận xét giống như dịch vụ khách hàng: nếu khách hàng của bạn cần dựa vào họ, thì sản phẩm chính của bạn bị lỗi.
  • Cho dù một cái gì đó là một giao diện hay một lớp chỉ có liên quan khi mở rộng hoặc triển khai nó. Với một thể hiện (đối tượng), 99% mã máy khách thời gian sẽ không biết hoặc không quan tâm xem thứ gì đó là giao diện hay một lớp. Lúc đầu, tôi cho rằng ImmutableMap là một giao diện. Chỉ sau khi tôi nhấp vào liên kết, tôi mới nhận ra đó là một lớp học.

5

Nó phụ thuộc vào chính lớp học. Guava's ImmutableMapkhông phải là một cái nhìn bất biến thành một lớp có thể thay đổi. Nếu lớp của bạn là bất biến và có một số cấu trúc về cơ bản là an ImmutableMap, thì hãy tạo kiểu trả về ImmutableMap. Tuy nhiên, nếu lớp của bạn có thể thay đổi, thì không. Nếu bạn có cái này:

public ImmutableMap<String, Integer> foo() {
    return ImmutableMap.copyOf(internalMap);
}

Ổi sẽ sao chép bản đồ mọi lúc. Thật chậm. Nhưng nếu internalMapđã là một ImmutableMap, thì nó hoàn toàn ổn.

Nếu bạn không hạn chế việc quay lại lớp của mình ImmutableMap, thì thay vào đó bạn có thể quay lại Collections.unmodifiableMapnhư vậy:

public Map<String, Integer> foo() {
    return Collections.unmodifiableMap(internalMap);
}

Lưu ý rằng đây là chế độ xem bất biến đối với bản đồ. Nếu internalMapthay đổi, bản sao được lưu trong bộ nhớ cache cũng vậy Collections.unmodifiableMap(internalMap). Tuy nhiên, tôi vẫn thích nó hơn cho người dân.


1
Đây là những điểm tốt, nhưng về tính đầy đủ, tôi thích câu trả lời này giải thích rằng unmodifiableMapngười nắm giữ tham chiếu duy nhất không thể sửa đổi - đó là một chế độ xem và người nắm giữ bản đồ hỗ trợ có thể sửa đổi bản đồ hỗ trợ và sau đó chế độ xem thay đổi. Vì vậy, đó là một cân nhắc quan trọng.
davidbak

@ Như davidback đã nhận xét, hãy rất cẩn thận trong việc sử dụng Collections.unmodifiableMap. Phương thức đó trả về một dạng xem trên Bản đồ gốc thay vì tạo một đối tượng bản đồ mới riêng biệt. Vì vậy, bất kỳ mã nào khác tham chiếu đến Bản đồ gốc đều có thể sửa đổi nó, và sau đó người nắm giữ bản đồ được cho là không thể sửa đổi sẽ học được cách khó mà bản đồ thực sự có thể được sửa đổi từ chúng. Tôi đã bị một chút bởi thực tế đó bản thân mình. Tôi đã học cách trả lại bản sao của Bản đồ hoặc sử dụng Guava ImmutableMap.
Basil Bourque vào

5

Đây không phải là câu trả lời chính xác cho câu hỏi, nhưng vẫn đáng xem xét liệu có nên trả lại bản đồ hay không. Nếu bản đồ là bất biến, thì phương thức chính được cung cấp dựa trên get (key):

public Integer fooOf(String key) {
    return map.get(key);
}

Điều này làm cho API chặt chẽ hơn nhiều. Nếu bản đồ thực sự được yêu cầu, điều này có thể được để cho ứng dụng khách của API bằng cách cung cấp một luồng mục nhập:

public Stream<Map.Entry<String, Integer>> foos() {
    map.entrySet().stream()
}

Sau đó, khách hàng có thể tạo bản đồ bất biến hoặc có thể thay đổi của riêng mình khi cần, hoặc thêm các mục vào bản đồ của riêng mình. Nếu khách hàng cần biết giá trị có tồn tại hay không, thay vào đó có thể trả về tùy chọn:

public Optional<Integer> fooOf(String key) {
    return Optional.ofNullable(map.get(key));
}

-1

Bản đồ bất biến là một loại Bản đồ. Vì vậy để lại kiểu trả về của Map là được.

Để đảm bảo rằng người dùng không sửa đổi đối tượng được trả về, tài liệu của phương thức có thể mô tả các đặc điểm của đối tượng được trả về.


-1

Đây được cho là một vấn đề quan điểm, nhưng ý tưởng tốt hơn ở đây là sử dụng một giao diện cho lớp bản đồ. Giao diện này không cần phải nói rõ ràng rằng nó là bất biến, nhưng thông báo sẽ giống nhau nếu bạn không hiển thị bất kỳ phương thức setter nào của lớp cha trong giao diện.

Hãy xem bài viết sau:

andy gibson


1
Các trình bao bọc không cần thiết được coi là có hại.
djechlin

Bạn nói đúng, tôi cũng sẽ không sử dụng một trình bao bọc ở đây, vì giao diện là đủ.
WSimpson

Tôi có cảm giác nếu đây là điều đúng đắn cần làm thì ImmutableMap sẽ triển khai ImmutableMapInterface rồi, nhưng tôi không hiểu về mặt kỹ thuật này.
djechlin

Vấn đề không phải là những gì các nhà phát triển Guava dự định, đó là thực tế là nhà phát triển này muốn đóng gói kiến ​​trúc và không tiết lộ việc sử dụng ImmutableMap. Một cách để làm điều này là sử dụng một giao diện. Sử dụng một trình bao bọc như bạn đã chỉ ra là không cần thiết và do đó không được khuyến khích. Việc trả lại bản đồ là không mong muốn vì nó gây hiểu nhầm. Vì vậy, có vẻ như nếu mục tiêu là đóng gói, thì Giao diện là lựa chọn tốt nhất.
WSimpson

Điểm bất lợi của việc trả về thứ gì đó không mở rộng (hoặc triển khai) Mapgiao diện là bạn không thể chuyển nó đến bất kỳ hàm API nào mong đợi a Mapmà không truyền nó. Điều này bao gồm tất cả các loại trình tạo bản sao của các loại bản đồ khác. Ngoài ra, nếu khả năng thay đổi chỉ bị "ẩn" bởi giao diện, người dùng của bạn luôn có thể giải quyết vấn đề này bằng cách truyền và phá vỡ mã của bạn theo cách đó.
Hulk
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.