Triển khai tốt nhất cho phương thức hashCode cho một bộ sưu tập


299

Làm thế nào để chúng ta quyết định thực hiện tốt nhất hashCode()phương thức cho một bộ sưu tập (giả sử rằng phương thức bằng đã được ghi đè chính xác)?


2
với Java 7+, tôi đoán Objects.hashCode(collection)nên là một giải pháp hoàn hảo!
Diablo

3
@Diablo Tôi không nghĩ rằng câu trả lời của câu hỏi ở tất cả - phương pháp mà chỉ đơn giản là lợi nhuận collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/... )
cbreezier

Câu trả lời:


438

Việc thực hiện tốt nhất? Đó là một câu hỏi khó vì nó phụ thuộc vào mô hình sử dụng.

Một gần như cho tất cả các trường hợp triển khai tốt hợp lý đã được đề xuất trong Java hiệu quả của Josh Bloch trong Mục 8 (phiên bản thứ hai). Điều tốt nhất là tìm nó ở đó bởi vì tác giả giải thích tại sao cách tiếp cận là tốt.

Một phiên bản ngắn

  1. Tạo một int resultvà gán một giá trị khác không .

  2. Đối với mọi trường f được kiểm tra trong equals()phương thức, hãy tính mã băm cbằng cách:

    • Nếu trường f là a boolean: tính toán (f ? 0 : 1);
    • Nếu trường f là một byte, char, shorthay int: tính toán (int)f;
    • Nếu trường f là a long: tính toán (int)(f ^ (f >>> 32));
    • Nếu trường f là a float: tính toán Float.floatToIntBits(f);
    • Nếu trường f là a double: tính toán Double.doubleToLongBits(f)và xử lý giá trị trả về như mọi giá trị dài;
    • Nếu trường f là một đối tượng : Sử dụng kết quả của hashCode()phương thức hoặc 0 if f == null;
    • Nếu trường f là một mảng : xem mọi trường là phần tử riêng biệt và tính giá trị băm theo kiểu đệ quy và kết hợp các giá trị như được mô tả tiếp theo.
  3. Kết hợp giá trị băm cvới result:

    result = 37 * result + c
  4. Trở về result

Điều này sẽ dẫn đến một phân phối giá trị băm thích hợp cho hầu hết các tình huống sử dụng.


45
Vâng, tôi đặc biệt tò mò về số 37 đến từ đâu.
Kip

17
Tôi đã sử dụng mục 8 của cuốn sách "Java hiệu quả" của Josh Bloch.
dmeister

39
@dma_k Lý do sử dụng số nguyên tố và phương pháp được mô tả trong câu trả lời này là để đảm bảo rằng mã băm được tính toán sẽ là duy nhất . Khi sử dụng các số không phải là số nguyên tố, bạn không thể đảm bảo điều này. Không quan trọng bạn chọn số nguyên tố nào, không có gì kỳ diệu về số 37 (quá tệ 42 không phải là số nguyên tố, eh?)
Simon Forsberg

34
@ SimonAndréForsberg Chà, mã băm được tính toán không thể luôn là duy nhất :) Là mã băm. Tuy nhiên tôi có ý tưởng: số nguyên tố chỉ có một số nhân, trong khi số nguyên tố có ít nhất hai số. Điều đó tạo ra một sự kết hợp bổ sung cho toán tử nhân để tạo ra cùng một hàm băm, nghĩa là gây ra xung đột.
dma_k


140

Nếu bạn hài lòng với triển khai Java hiệu quả được đề xuất bởi dmeister, bạn có thể sử dụng lệnh gọi thư viện thay vì tự thực hiện:

@Override
public int hashCode() {
    return Objects.hashCode(this.firstName, this.lastName);
}

Điều này đòi hỏi Guava ( com.google.common.base.Objects.hashCode) hoặc thư viện chuẩn trong Java 7 ( java.util.Objects.hash) nhưng hoạt động theo cùng một cách.


8
Trừ khi một người có lý do chính đáng để không sử dụng những thứ này, chắc chắn người ta nên sử dụng chúng trong mọi trường hợp. (Xây dựng nó mạnh hơn, vì IMHO nên được xây dựng.) Các đối số điển hình cho việc sử dụng triển khai / thư viện tiêu chuẩn được áp dụng (thực tiễn tốt nhất, được kiểm tra tốt, ít bị lỗi, v.v.).
Kissaki

7
@ justin.hughey bạn có vẻ bối rối. Trường hợp duy nhất bạn nên ghi đè hashCodelà nếu bạn có một tùy chỉnh equalsvà đó chính xác là những gì các phương thức thư viện này được thiết kế cho. Các tài liệu khá rõ ràng về hành vi của họ liên quan đến equals. Việc triển khai thư viện không yêu cầu bạn miễn trừ việc biết các đặc điểm của hashCodeviệc triển khai chính xác là gì - những thư viện này giúp bạn dễ dàng thực hiện việc triển khai tuân thủ như vậy đối với phần lớn các trường hợp equalsbị chồng chéo.
thịt xông khói

6
Đối với bất kỳ nhà phát triển Android nào đang xem lớp java.util.Objects, nó chỉ được giới thiệu trong API 19, vì vậy hãy đảm bảo bạn đang chạy trên KitKat trở lên nếu không bạn sẽ nhận được NoClassDefFoundError.
Andrew Kelly

3
IMO trả lời tốt nhất, mặc dù bằng ví dụ, tôi thà chọn java.util.Objects.hash(...)phương thức JDK7 hơn là com.google.common.base.Objects.hashCode(...)phương pháp ổi . Tôi nghĩ rằng hầu hết mọi người sẽ chọn thư viện tiêu chuẩn thay vì phụ thuộc thêm.
Malte Skoruppa

2
Nếu có hai đối số trở lên và nếu bất kỳ trong số chúng là một mảng, kết quả có thể không như bạn mong đợi vì hashCode()đối với một mảng chỉ là của nó java.lang.System.identityHashCode(...).
starikoff

59

Tốt hơn là sử dụng chức năng do Eclipse cung cấp, một công việc khá tốt và bạn có thể nỗ lực và năng lượng của mình để phát triển logic kinh doanh.


4
+1 Một giải pháp thực tế tốt. Giải pháp của dmeister là toàn diện hơn, nhưng tôi có xu hướng quên xử lý null khi tôi cố gắng tự viết mã băm.
Quantum7

1
+1 Đồng ý với Quantum7, nhưng tôi sẽ nói rằng thật sự rất tốt để hiểu việc triển khai do Eclipse tạo ra đang làm gì và nó lấy chi tiết triển khai từ đâu.
jwir3

15
Xin lỗi nhưng câu trả lời liên quan đến "chức năng được cung cấp bởi [một số IDE]" không thực sự phù hợp trong bối cảnh ngôn ngữ lập trình nói chung. Có hàng tá IDE và điều này không trả lời được câu hỏi ... cụ thể là vì đây là về xác định thuật toán và liên quan trực tiếp đến việc thực hiện bằng () - một điều mà IDE sẽ không biết gì.
Darrell Teague

57

Mặc dù điều này được liên kết với Androidtài liệu (Wayback Machine)mã riêng của tôi trên Github , nhưng nó sẽ hoạt động cho Java nói chung. Câu trả lời của tôi là phần mở rộng của Câu trả lời của dmeister chỉ với mã dễ đọc và dễ hiểu hơn nhiều.

@Override 
public int hashCode() {

    // Start with a non-zero constant. Prime is preferred
    int result = 17;

    // Include a hash for each field.

    // Primatives

    result = 31 * result + (booleanField ? 1 : 0);                   // 1 bit   » 32-bit

    result = 31 * result + byteField;                                // 8 bits  » 32-bit 
    result = 31 * result + charField;                                // 16 bits » 32-bit
    result = 31 * result + shortField;                               // 16 bits » 32-bit
    result = 31 * result + intField;                                 // 32 bits » 32-bit

    result = 31 * result + (int)(longField ^ (longField >>> 32));    // 64 bits » 32-bit

    result = 31 * result + Float.floatToIntBits(floatField);         // 32 bits » 32-bit

    long doubleFieldBits = Double.doubleToLongBits(doubleField);     // 64 bits (double) » 64-bit (long) » 32-bit (int)
    result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));

    // Objects

    result = 31 * result + Arrays.hashCode(arrayField);              // var bits » 32-bit

    result = 31 * result + referenceField.hashCode();                // var bits » 32-bit (non-nullable)   
    result = 31 * result +                                           // var bits » 32-bit (nullable)   
        (nullableReferenceField == null
            ? 0
            : nullableReferenceField.hashCode());

    return result;

}

BIÊN TẬP

Thông thường, khi bạn ghi đè hashcode(...), bạn cũng muốn ghi đè equals(...). Vì vậy, đối với những người sẽ hoặc đã thực hiện equals, đây là một tài liệu tham khảo tốt từ Github của tôi ...

@Override
public boolean equals(Object o) {

    // Optimization (not required).
    if (this == o) {
        return true;
    }

    // Return false if the other object has the wrong type, interface, or is null.
    if (!(o instanceof MyType)) {
        return false;
    }

    MyType lhs = (MyType) o; // lhs means "left hand side"

            // Primitive fields
    return     booleanField == lhs.booleanField
            && byteField    == lhs.byteField
            && charField    == lhs.charField
            && shortField   == lhs.shortField
            && intField     == lhs.intField
            && longField    == lhs.longField
            && floatField   == lhs.floatField
            && doubleField  == lhs.doubleField

            // Arrays

            && Arrays.equals(arrayField, lhs.arrayField)

            // Objects

            && referenceField.equals(lhs.referenceField)
            && (nullableReferenceField == null
                        ? lhs.nullableReferenceField == null
                        : nullableReferenceField.equals(lhs.nullableReferenceField));
}

1
Tài liệu Android hiện không bao gồm mã ở trên nữa, vì vậy đây là phiên bản được lưu trong bộ nhớ cache từ Wayback Machine - Tài liệu Android (ngày 07 tháng 2 năm 2015)
Christopher Rucinski

17

Trước tiên hãy chắc chắn rằng bằng được thực hiện chính xác. Từ một bài viết IBM DeveloperWorks :

  • Đối xứng: Cho hai tham chiếu, a và b, a.equals (b) khi và chỉ khi b.equals (a)
  • Tính phản xạ: Đối với tất cả các tham chiếu không null, a.equals (a)
  • Độ biến đổi: Nếu a.equals (b) và b.equals (c), thì a.equals (c)

Sau đó, đảm bảo rằng mối quan hệ của họ với hashCode tôn trọng liên hệ (từ cùng một bài viết):

  • Tính nhất quán với hashCode (): Hai đối tượng bằng nhau phải có cùng giá trị hashCode ()

Cuối cùng, một hàm băm tốt nên cố gắng tiếp cận hàm băm lý tưởng .


11

about8.blogspot.com, bạn đã nói

if equals () trả về true cho hai đối tượng, thì hashCode () sẽ trả về cùng một giá trị. Nếu bằng () trả về false, thì hashCode () sẽ trả về các giá trị khác nhau

Tôi không thể đồng ý với bạn. Nếu hai đối tượng có cùng mã băm thì không có nghĩa là chúng bằng nhau.

Nếu A bằng B thì A.hashcode phải bằng B.hascode

nhưng

nếu A.hashcode bằng B.hascode thì không có nghĩa là A phải bằng B


3
Nếu (A != B) and (A.hashcode() == B.hashcode()), đó là những gì chúng ta gọi là va chạm hàm băm. Đó là vì tên miền của hàm băm luôn hữu hạn, trong khi tên miền thường thì không. Tên miền càng lớn thì càng ít xảy ra va chạm. Hàm băm tốt sẽ trả về các giá trị băm khác nhau cho các đối tượng khác nhau với khả năng lớn nhất có thể đạt được với kích thước tên miền cụ thể. Nó hiếm khi có thể được đảm bảo đầy đủ mặc dù.
Krzysztof Jabłoński

Điều này chỉ nên là một bình luận cho bài viết trên cho Gray. Thông tin tốt nhưng nó không thực sự trả lời câu hỏi
Christopher Rucinski

Nhận xét tốt nhưng hãy cẩn thận về việc sử dụng thuật ngữ 'các đối tượng khác nhau' ... bởi vì việc triển khai bằng () và do đó, việc triển khai hashCode () không nhất thiết là về các đối tượng khác nhau trong ngữ cảnh OO mà thường là về các biểu diễn mô hình miền của chúng (ví dụ: hai mọi người có thể được coi là giống nhau nếu họ chia sẻ mã quốc gia và ID quốc gia - mặc dù đây có thể là hai 'đối tượng' khác nhau trong JVM - chúng được coi là 'bằng nhau' và có mã băm nhất định) ...
Darrell Teague

7

Nếu bạn sử dụng nhật thực, bạn có thể tạo equals()hashCode()sử dụng:

Nguồn -> Tạo hashCode () và bằng ().

Sử dụng hàm này, bạn có thể quyết định trường nào bạn muốn sử dụng để tính toán bằng mã và hàm băm và Eclipse tạo ra các phương thức tương ứng.


7

Có một thực hiện tốt các hiệu quả Java 's hashcode()equals()logic trong Apache Commons Lang . Thanh toán HashCodeBuilderEqualsBuilder .


1
Nhược điểm của API này là bạn phải trả chi phí xây dựng đối tượng mỗi khi bạn gọi bằng và mã băm (trừ khi đối tượng của bạn là bất biến và bạn tính toán trước hàm băm), có thể rất nhiều trong một số trường hợp nhất định.
James McMahon

đây là cách tiếp cận yêu thích của tôi, cho đến gần đây. Tôi đã chạy vào StackOverFlowError trong khi sử dụng một tiêu chí cho hiệp hội SharedKey OneToOne. Hơn nữa, Objectslớp cung cấp hash(Object ..args)& equals()phương thức từ Java7 trở đi. Chúng được khuyến nghị cho mọi ứng dụng sử dụng jdk 1.7+
Diablo

@Diablo Tôi đoán, vấn đề của bạn là một chu kỳ trong biểu đồ đối tượng và sau đó bạn không gặp may mắn với hầu hết việc thực hiện vì bạn cần bỏ qua một số tham chiếu hoặc để phá vỡ chu trình (bắt buộc một IdentityHashMap). FWIW Tôi sử dụng mã băm dựa trên id và bằng với tất cả các thực thể.
maaartinus

6

Chỉ cần một lưu ý nhanh để hoàn thành câu trả lời chi tiết khác (về mã):

Nếu tôi xem xét câu hỏi how-do-i- created -a-hash-table-in-java và đặc biệt là mục FAQ của jGuru , tôi tin rằng một số tiêu chí khác mà mã băm có thể được đánh giá là:

  • đồng bộ hóa (có phải algo hỗ trợ truy cập đồng thời hay không)?
  • không lặp lại an toàn (thuật toán có phát hiện ra một bộ sưu tập thay đổi trong quá trình lặp không)
  • null value (mã băm có hỗ trợ giá trị null trong bộ sưu tập không)

4

Nếu tôi hiểu chính xác câu hỏi của bạn, bạn có một lớp bộ sưu tập tùy chỉnh (nghĩa là một lớp mới mở rộng từ giao diện Bộ sưu tập) và bạn muốn thực hiện phương thức hashCode ().

Nếu lớp bộ sưu tập của bạn mở rộng AbstractList, thì bạn không phải lo lắng về nó, đã có một triển khai của Equals () và hashCode () hoạt động bằng cách lặp qua tất cả các đối tượng và thêm hashCodes () của chúng với nhau.

   public int hashCode() {
      int hashCode = 1;
      Iterator i = iterator();
      while (i.hasNext()) {
        Object obj = i.next();
        hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
      }
  return hashCode;
   }

Bây giờ nếu những gì bạn muốn là cách tốt nhất để tính mã băm cho một lớp cụ thể, tôi thường sử dụng toán tử ^ (bitwise độc ​​quyền hoặc) để xử lý tất cả các trường mà tôi sử dụng trong phương thức bằng:

public int hashCode(){
   return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}

2

@ about8: có một lỗi khá nghiêm trọng đấy.

Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");

cùng mã băm

bạn có thể muốn một cái gì đó như

public int hashCode() {
    return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();

.


3
lỗi nằm trong câu trả lời dài của about8.blogspot.com - việc lấy mã băm từ một chuỗi nối để lại cho bạn một hàm băm giống nhau cho bất kỳ tổ hợp chuỗi nào thêm vào cùng một chuỗi.
SquareCog

1
Vì vậy, đây là thảo luận meta và không liên quan đến câu hỏi ở tất cả? ;-)
Huppie

1
Đó là một sự điều chỉnh cho một câu trả lời được đề xuất có một lỗ hổng khá quan trọng.
SquareCog

Đây là một triển khai rất hạn chế
Christopher Rucinski

Việc thực hiện của bạn tránh được vấn đề và giới thiệu một vấn đề khác; Trao đổi foobardẫn đến cùng hashCode. toStringAFAIK của bạn không biên dịch, và nếu có, thì nó không hiệu quả. Một cái gì đó như 109 * getFoo().hashCode() + 57 * getBar().hashCode()là nhanh hơn, đơn giản hơn và không tạo ra va chạm không cần thiết.
maaartinus

2

Như bạn đã yêu cầu cụ thể về các bộ sưu tập, tôi muốn thêm một khía cạnh mà các câu trả lời khác chưa được đề cập: HashMap không mong đợi các khóa của họ thay đổi mã băm sau khi chúng được thêm vào bộ sưu tập. Sẽ đánh bại toàn bộ mục đích ...


2

Sử dụng các phương thức phản chiếu trên Apache Commons EqualsBuilderHashCodeBuilder .


1
Nếu bạn sẽ sử dụng điều này hãy lưu ý rằng sự phản chiếu là tốn kém. Tôi thực sự sẽ không sử dụng điều này cho bất cứ điều gì ngoài việc vứt bỏ mã.
James McMahon

2

Tôi sử dụng một trình bao bọc nhỏ xung quanh Arrays.deepHashCode(...)vì nó xử lý các mảng được cung cấp dưới dạng tham số chính xác

public static int hash(final Object... objects) {
    return Arrays.deepHashCode(objects);
}


1

Tôi thích sử dụng các phương thức tiện ích từ Google Bộ sưu tập lib từ các Đối tượng lớp giúp tôi giữ sạch mã của mình. Rất thường xuyên equalshashcodecác phương thức được tạo từ mẫu của IDE, vì vậy chúng không sạch để đọc.


1

Dưới đây là một trình diễn tiếp cận JDK 1.7+ khác với các logic siêu lớp được hạch toán. Tôi thấy nó khá thuận tiện với tài khoản hashCode () của lớp Object, phụ thuộc JDK thuần túy và không có công việc thủ công thêm. Xin lưu ýObjects.hash() là không khoan dung.

Tôi không bao gồm bất kỳ equals()thực hiện nhưng trong thực tế, bạn tất nhiên sẽ cần nó.

import java.util.Objects;

public class Demo {

    public static class A {

        private final String param1;

        public A(final String param1) {
            this.param1 = param1;
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param1);
        }

    }

    public static class B extends A {

        private final String param2;
        private final String param3;

        public B(
            final String param1,
            final String param2,
            final String param3) {

            super(param1);
            this.param2 = param2;
            this.param3 = param3;
        }

        @Override
        public final int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param2,
                this.param3);
        }
    }

    public static void main(String [] args) {

        A a = new A("A");
        B b = new B("A", "B", "C");

        System.out.println("A: " + a.hashCode());
        System.out.println("B: " + b.hashCode());
    }

}

1

Việc thực hiện tiêu chuẩn là yếu và sử dụng nó dẫn đến va chạm không cần thiết. Hãy tưởng tượng một

class ListPair {
    List<Integer> first;
    List<Integer> second;

    ListPair(List<Integer> first, List<Integer> second) {
        this.first = first;
        this.second = second;
    }

    public int hashCode() {
        return Objects.hashCode(first, second);
    }

    ...
}

Hiện nay,

new ListPair(List.of(a), List.of(b, c))

new ListPair(List.of(b), List.of(a, c))

có cùng hashCode, cụ thể 31*(a+b) + clà số nhân được sử dụng choList.hashCode được tái sử dụng ở đây. Rõ ràng, va chạm là không thể tránh khỏi, nhưng tạo ra va chạm không cần thiết chỉ là ... không cần thiết.

Không có gì đáng kể thông minh về việc sử dụng 31. Số nhân phải là số lẻ để tránh mất thông tin (bất kỳ số nhân chẵn nào cũng mất ít nhất là bit đáng kể nhất, bội số của bốn mất hai, v.v.). Bất kỳ số nhân lẻ đều có thể sử dụng. Số nhân nhỏ có thể dẫn đến tính toán nhanh hơn (JIT có thể sử dụng dịch chuyển và bổ sung), nhưng với điều kiện nhân có độ trễ chỉ ba chu kỳ trên Intel / AMD hiện đại, điều này hầu như không thành vấn đề. Số nhân nhỏ cũng dẫn đến xung đột nhiều hơn cho đầu vào nhỏ, đôi khi có thể là một vấn đề.

Sử dụng một số nguyên tố là vô nghĩa vì các số nguyên tố không có ý nghĩa trong vòng Z / (2 ** 32).

Vì vậy, tôi khuyên bạn nên sử dụng một số lẻ lớn được chọn ngẫu nhiên (thoải mái lấy số nguyên tố). Vì CPU i86 / amd64 có thể sử dụng một lệnh ngắn hơn cho các toán hạng phù hợp trong một byte đã ký, có một lợi thế tốc độ nhỏ cho các số nhân như 109. Để giảm thiểu va chạm, hãy lấy thứ gì đó như 0x58a54cf5.

Sử dụng các số nhân khác nhau ở những nơi khác nhau là hữu ích, nhưng có lẽ không đủ để biện minh cho công việc bổ sung.


0

Khi kết hợp các giá trị băm, tôi thường sử dụng phương thức kết hợp được sử dụng trong thư viện boost c ++, cụ thể là:

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

Điều này làm một công việc khá tốt để đảm bảo phân phối đồng đều. Để biết một số thảo luận về cách thức hoạt động của công thức này, hãy xem bài đăng StackOverflow: Số ma thuật trong boost :: hash_combine

Có một cuộc thảo luận tốt về các hàm băm khác nhau tại: http://burtleburtle.net/bob/hash/doobs.html


1
Đây là một câu hỏi về Java, không phải C ++.
dano

-1

Đối với một lớp đơn giản, thường dễ thực hiện hàm hashCode () dựa trên các trường lớp được kiểm tra bằng cách thực hiện bằng ().

public class Zam {
    private String foo;
    private String bar;
    private String somethingElse;

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null) {
            return false;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        Zam otherObj = (Zam)obj;

        if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
            if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
                return true;
            }
        }

        return false;
    }

    public int hashCode() {
        return (getFoo() + getBar()).hashCode();
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

Điều quan trọng nhất là giữ cho hashCode () và bằng () nhất quán: nếu bằng () trả về true cho hai đối tượng, thì hashCode () sẽ trả về cùng một giá trị. Nếu bằng () trả về false, thì hashCode () sẽ trả về các giá trị khác nhau.


1
Giống như SquareCog đã nhận thấy. Nếu mã băm được tạo một lần từ nối hai chuỗi, cực kỳ dễ tạo ra các khối va chạm : ("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc"). Đó là lỗ hổng nghiêm trọng. Sẽ tốt hơn khi đánh giá mã băm cho cả hai trường và sau đó tính toán kết hợp tuyến tính của chúng (tốt nhất là sử dụng các số nguyên tố làm hệ số).
Krzysztof Jabłoński

@ KrzysztofJabłoński Phải. Hơn nữa, trao đổi foobartạo ra một sự va chạm không cần thiết, quá.
maaartinus
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.