Làm cách nào để ngăn chặn sửa đổi trường riêng trong một lớp?


165

Hãy tưởng tượng rằng tôi có lớp học này:

public class Test
{
  private String[] arr = new String[]{"1","2"};    

  public String[] getArr() 
  {
    return arr;
  }
}

Bây giờ, tôi có một lớp khác sử dụng lớp trên:

Test test = new Test();
test.getArr()[0] ="some value!"; //!!!

Vì vậy, đây là vấn đề: Tôi đã truy cập vào một trường riêng của một lớp từ bên ngoài! Làm thế nào tôi có thể ngăn chặn điều này? Tôi có nghĩa là làm thế nào tôi có thể làm cho mảng này bất biến? Điều này có nghĩa là với mọi phương thức getter, bạn có thể làm việc theo cách của mình để truy cập vào trường riêng? (Tôi không muốn bất kỳ thư viện nào như Guava. Tôi chỉ cần biết đúng cách để làm điều này).


10
Trên thực tế, làm cho nó finalkhông ngăn chặn sự thay đổi của lĩnh vực này . Tuy nhiên, việc ngăn chặn sửa đổi Objectđược đề cập bởi một lĩnh vực phức tạp hơn.
OldCurmudgeon

22
Có một vấn đề với mô hình tinh thần của bạn nếu bạn nghĩ rằng có thể sửa đổi một mảng mà bạn có một tham chiếu được lưu trữ trong một trường riêng cũng giống như có thể sửa đổi một trường riêng.
Joren

45
Nếu nó là riêng tư, tại sao lại phơi nó ở nơi đầu tiên?
Erik Reppen

7
Tôi đã mất một lúc để tìm ra điều này nhưng nó luôn cải thiện mã của tôi khi tôi đảm bảo rằng tất cả các cấu trúc dữ liệu được gói gọn trong đối tượng của chúng - có nghĩa là không có cách nào để "lấy" mảng "của bạn, thay vào đó, bạn phải ở bên trong lớp hoặc cung cấp một iterator.
Bill K

8
Có ai khác cũng ngạc nhiên tại sao câu hỏi này lại được chú ý đến vậy?
La Mã

Câu trả lời:


163

Bạn phải trả lại một bản sao của mảng của bạn.

public String[] getArr() {
  return arr == null ? null : Arrays.copyOf(arr, arr.length);
}

121
Tôi muốn nhắc nhở mọi người rằng mặc dù điều này đã được bình chọn là câu trả lời chính xác cho câu hỏi, nhưng giải pháp tốt nhất cho vấn đề thực sự là như sp00m nói - trả lại một Unmodifiable List.
OldCurmudgeon

48
Không, nếu bạn thực sự cần phải trả về một mảng.
Svish

8
Trên thực tế, giải pháp tốt nhất cho vấn đề được thảo luận thường như @MikhailVladimirov nói: - để cung cấp hoặc trả về một cái nhìn về mảng hoặc bộ sưu tập.
Christoffer Hammarström

20
nhưng hãy chắc chắn rằng đó là một bản sao sâu chứ không phải bản sao nông của dữ liệu. Đối với Chuỗi, điều này không có gì khác biệt, đối với các lớp khác như Ngày mà nội dung của thể hiện có thể được sửa đổi, nó có thể tạo ra sự khác biệt.
jwenting

16
@Svish Tôi nên quyết liệt hơn: nếu bạn trả về một mảng từ hàm API thì bạn đã làm sai. Trong các chức năng riêng tư trong một thư viện, nó thể đúng. Trong một API (trong đó bảo vệ chống lại sửa đổi đóng vai trò), nó không bao giờ .
Konrad Rudolph

377

Nếu bạn có thể sử dụng Danh sách thay vì mảng, Bộ sưu tập cung cấp danh sách không thể thay đổi :

public List<String> getList() {
    return Collections.unmodifiableList(list);
}

Đã thêm một câu trả lời sử dụng Collections.unmodifiableList(list)như gán cho trường, để đảm bảo rằng danh sách không bị thay đổi bởi chính lớp đó.
Maarten Bodewes

Chỉ cần tự hỏi, nếu bạn dịch ngược phương pháp này, bạn sẽ nhận được rất nhiều từ khóa mới. Điều này không ảnh hưởng đến hiệu suất khi getList () có quyền truy cập nhiều. Ví dụ trong mã kết xuất.
Thomas15v

2
Lưu ý rằng điều này hoạt động nếu bản thân các thành phần của mảng là bất biến String, nhưng nếu chúng có thể thay đổi thì có thể phải thực hiện để ngăn người gọi thay đổi chúng.
Lii

45

Công cụ sửa đổi privatechỉ bảo vệ trường khỏi bị truy cập từ các lớp khác, nhưng không phải các tham chiếu đối tượng của trường này. Nếu bạn cần bảo vệ đối tượng được tham chiếu, chỉ cần không đưa ra. Thay đổi

public String [] getArr ()
{
    return arr;
}

đến:

public String [] getArr ()
{
    return arr.clone ();
}

hoặc để

public int getArrLength ()
{
    return arr.length;
}

public String getArrElementAt (int index)
{
    return arr [index];
}

5
Bài viết này không tranh luận về việc sử dụng bản sao trên mảng, chỉ trên các đối tượng có thể được phân lớp.
Lyn Headley

28

Điều Collections.unmodifiableListnày đã được đề cập - Arrays.asList()thật lạ là không! Giải pháp của tôi cũng sẽ là sử dụng danh sách từ bên ngoài và bọc mảng như sau:

String[] arr = new String[]{"1", "2"}; 
public List<String> getList() {
    return Collections.unmodifiableList(Arrays.asList(arr));
}

Vấn đề với việc sao chép mảng là: nếu bạn đang thực hiện nó mỗi khi bạn truy cập mã và mảng lớn, chắc chắn bạn sẽ tạo ra rất nhiều công việc cho trình thu gom rác. Vì vậy, bản sao là một cách tiếp cận đơn giản nhưng thực sự tồi tệ - tôi muốn nói "rẻ", nhưng đắt bộ nhớ! Đặc biệt là khi bạn có nhiều hơn chỉ 2 yếu tố.

Nếu bạn nhìn vào mã nguồn của Arrays.asListCollections.unmodifiableListthực sự không có nhiều thứ được tạo ra. Cái đầu tiên chỉ bao bọc mảng mà không sao chép nó, cái thứ hai chỉ bao bọc danh sách, thay đổi nó không có sẵn.


Bạn đưa ra giả định ở đây rằng mảng bao gồm các chuỗi được sao chép mỗi lần. Không, chỉ có các tham chiếu đến các chuỗi bất biến được sao chép. Miễn là mảng của bạn không lớn, chi phí không đáng kể. Nếu mảng là rất lớn, hãy tìm danh sách và immutableListtất cả các cách!
Maarten Bodewes

Không, tôi đã không đưa ra giả định đó - ở đâu? Đó là về các tham chiếu được sao chép - không quan trọng nếu đó là Chuỗi hoặc bất kỳ đối tượng nào khác. Chỉ cần nói rằng đối với các mảng lớn, tôi không khuyên bạn nên sao chép mảng và các hoạt động Arrays.asListCollections.unmodifiableListcó thể rẻ hơn cho các mảng lớn hơn. Có lẽ ai đó có thể tính toán có bao nhiêu phần tử là cần thiết, nhưng tôi đã thấy mã trong các vòng lặp với các mảng chứa hàng ngàn phần tử được sao chép chỉ để ngăn sửa đổi - điều đó thật điên rồ!
michael_s

6

Bạn cũng có thể sử dụng ImmutableListnên tốt hơn so với tiêu chuẩn unmodifiableList. Lớp này là một phần của các thư viện Guava do Google tạo ra.

Dưới đây là mô tả:

Không giống như Collections.unmodifiableList (java.util.List), là chế độ xem của một bộ sưu tập riêng biệt vẫn có thể thay đổi, một phiên bản của ImmutableList chứa dữ liệu riêng tư của nó và sẽ không bao giờ thay đổi

Đây là một ví dụ đơn giản về cách sử dụng nó:

public class Test
{
  private String[] arr = new String[]{"1","2"};    

  public ImmutableList<String> getArr() 
  {
    return ImmutableList.copyOf(arr);
  }
}

Sử dụng giải pháp này nếu bạn không thể tin tưởng Testlớp để danh sách trả về không bị thay đổi . Bạn cần chắc chắn rằng nó ImmutableListđược trả về, và các yếu tố của danh sách là bất biến. Nếu không, giải pháp của tôi cũng nên an toàn.
Maarten Bodewes

3

tại thời điểm này, bạn nên sử dụng bản sao hệ thống:

public String[] getArr() {
   if (arr != null) {
      String[] arrcpy = new String[arr.length];
      System.arraycopy(arr, 0, arrcpy, 0, arr.length);
      return arrcpy;
   } else
      return null;
   }
}

-1 vì nó không làm gì hơn khi gọi clone, và không ngắn gọn.
Maarten Bodewes

2

Bạn có thể trả lại một bản sao của dữ liệu. Người gọi chọn thay đổi dữ liệu sẽ chỉ thay đổi bản sao

public class Test {
    private static String[] arr = new String[] { "1", "2" };

    public String[] getArr() {

        String[] b = new String[arr.length];

        System.arraycopy(arr, 0, b, 0, arr.length);

        return b;
    }
}

2

Vấn đề không phải là bạn đang trả về một con trỏ tới một đối tượng có thể thay đổi. Giáo sư. Hoặc bạn kết xuất đối tượng không thay đổi (giải pháp danh sách không thể thay đổi) hoặc bạn trả về một bản sao của đối tượng.

Như một vấn đề chung, tính hữu hạn của các đối tượng không bảo vệ các đối tượng khỏi bị thay đổi nếu chúng có thể thay đổi. Hai vấn đề này là "hôn anh em họ."


Vâng Java có tài liệu tham khảo, trong khi C có con trỏ. Đủ nói. Hơn nữa, công cụ sửa đổi "cuối cùng" có thể có nhiều tác động tùy thuộc vào nơi nó được áp dụng. Áp dụng cho một thành viên lớp sẽ bắt buộc bạn chỉ gán giá trị cho thành viên chính xác một lần với toán tử "=" (gán). Điều này không có gì để làm với sự bất biến. Nếu bạn thay thế tham chiếu của một thành viên bằng một tham chiếu mới (một số thể hiện khác từ cùng một lớp), thì thể hiện trước đó sẽ không thay đổi (nhưng có thể được xử lý nếu không có tham chiếu nào khác giữ chúng).
gyorgyabraham

1

Trả lại một danh sách không thể thay đổi là một ý tưởng tốt. Nhưng một danh sách được tạo ra không thể thay đổi trong khi gọi phương thức getter vẫn có thể được thay đổi bởi lớp hoặc các lớp có nguồn gốc từ lớp.

Thay vào đó, bạn nên làm rõ với bất kỳ ai mở rộng lớp mà danh sách không nên sửa đổi.

Vì vậy, trong ví dụ của bạn, nó có thể dẫn đến đoạn mã sau:

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Test {
    public static final List<String> STRINGS =
        Collections.unmodifiableList(
            Arrays.asList("1", "2"));

    public final List<String> getStrings() {
        return STRINGS;
    }
}

Trong ví dụ trên tôi đã thực hiện STRINGStrường công khai, về nguyên tắc bạn có thể loại bỏ lệnh gọi phương thức, vì các giá trị đã được biết.

Bạn cũng có thể gán các chuỗi cho một private final List<String>trường được thực hiện không thể thay đổi trong quá trình xây dựng thể hiện của lớp. Sử dụng một đối số hằng hoặc khởi tạo (của hàm tạo) phụ thuộc vào thiết kế của lớp.

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Test {
    private final List<String> strings;

    public Test(final String ... strings) {
        this.strings = Collections.unmodifiableList(Arrays
                .asList(strings));
    }

    public final List<String> getStrings() {
        return strings;
    }
}

0

Có, bạn nên trả về một bản sao của mảng:

 public String[] getArr()
 {
    return Arrays.copyOf(arr);
 }
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.