Apache Commons bằng / hashCode builder [đã đóng]


155

Tôi tò mò muốn biết, mọi người ở đây nghĩ gì về việc sử dụng org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder để thực hiện equals/ hashCode? Nó sẽ là một thực hành tốt hơn so với viết của riêng bạn? Nó có chơi tốt với Hibernate không? Ý kiến ​​của bạn là gì?


16
Chỉ cần không bị cám dỗ bởi các chức năng reflectionEqualsreflectionHashcode; hiệu suất là một kẻ giết người tuyệt đối.
skaffman

14
Tôi đã thấy một số cuộc thảo luận ở đây về bằng ngày hôm qua và có một chút thời gian rảnh, vì vậy tôi đã làm một bài kiểm tra nhanh. Tôi đã có 4 đối tượng với các triển khai bằng nhau khác nhau. nhật thực được tạo ra, Equalsbuilder.append, Equalsbuilder.reflection và chú thích pojomatic. Đường cơ sở là nhật thực. Equalsbuilder.append mất 3,7 lần. pojomatic mất 5x. phản ánh dựa trên 25,8x. Điều đó khá nản lòng vì tôi thích sự đơn giản của sự phản chiếu dựa trên và tôi không thể chịu được cái tên "độc".
Digitaljoel

5
Một lựa chọn khác là Project Lombok; nó sử dụng việc tạo mã byte thay vì phản chiếu, vì vậy nó sẽ thực hiện tốt như được tạo bởi Eclipse. projectlombok.org/features/EqualsAndHashCode.html
Miles

Câu trả lời:


212

Các trình xây dựng commons / lang rất tuyệt vời và tôi đã sử dụng chúng trong nhiều năm mà không có hiệu suất đáng chú ý (có và không có ngủ đông). Nhưng như Alain viết, cách ổi thậm chí còn đẹp hơn:

Đây là một mẫu Bean:

public class Bean{

    private String name;
    private int length;
    private List<Bean> children;

}

Đây là bằng () và hashCode () được triển khai với Commons / Lang:

@Override
public int hashCode(){
    return new HashCodeBuilder()
        .append(name)
        .append(length)
        .append(children)
        .toHashCode();
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return new EqualsBuilder()
            .append(name, other.name)
            .append(length, other.length)
            .append(children, other.children)
            .isEquals();
    } else{
        return false;
    }
}

và ở đây với Java 7 trở lên (lấy cảm hứng từ Guava):

@Override
public int hashCode(){
    return Objects.hash(name, length, children);
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return Objects.equals(name, other.name)
            && length == other.length // special handling for primitives
            && Objects.equals(children, other.children);
    } else{
        return false;
    }
}

Lưu ý: mã này ban đầu được tham chiếu Guava, nhưng như các ý kiến ​​đã chỉ ra, chức năng này đã được giới thiệu trong JDK, do đó, Guava không còn cần thiết nữa.

Như bạn có thể thấy phiên bản Guava / JDK ngắn hơn và tránh các đối tượng trợ giúp không cần thiết. Trong trường hợp bằng, nó thậm chí còn cho phép đánh giá ngắn mạch nếu một Object.equals()cuộc gọi trước đó trả về sai (công bằng: commons / lang có một ObjectUtils.equals(obj1, obj2)phương thức với ngữ nghĩa giống hệt nhau có thể được sử dụng thay vì EqualsBuildercho phép đoản mạch như trên).

Vì vậy: có, các trình xây dựng lang commons rất thích hơn các phương thức được xây dựng thủ công equals()hashCode()các phương thức (hoặc những quái vật khủng khiếp mà Eclipse sẽ tạo ra cho bạn), nhưng các phiên bản Java 7+ / Guava thậm chí còn tốt hơn.

Và một lưu ý về Hibernate:

hãy cẩn thận về việc sử dụng các bộ sưu tập lười biếng trong các triển khai bằng (), hashCode () và toString () của bạn. Điều đó sẽ thất bại thảm hại nếu bạn không có Phiên mở.


Lưu ý (về bằng ()):

a) trong cả hai phiên bản bằng () ở trên, bạn cũng có thể muốn sử dụng một hoặc cả hai phím tắt này:

@Override
public boolean equals(final Object obj){
    if(obj == this) return true;  // test for reference equality
    if(obj == null) return false; // test for null
    // continue as above

b) tùy thuộc vào cách giải thích của bạn về hợp đồng bằng (), bạn cũng có thể thay đổi (các) dòng

    if(obj instanceof Bean){

đến

    // make sure you run a null check before this
    if(obj.getClass() == getClass()){ 

Nếu bạn sử dụng phiên bản thứ hai, có lẽ bạn cũng muốn gọi super(equals())bên trong equals()phương thức của mình . Ý kiến ​​khác nhau ở đây, chủ đề được thảo luận trong câu hỏi này:

đúng cách để kết hợp siêu lớp vào triển khai Guava Object.hashcode ()?

(mặc dù đó là về hashCode(), cùng áp dụng cho equals())


Lưu ý (lấy cảm hứng từ Nhận xét từ kayahr )

Objects.hashCode(..)(giống như bên dưới Arrays.hashCode(...)) có thể hoạt động kém nếu bạn có nhiều trường nguyên thủy. Trong những trường hợp như vậy, EqualsBuilderthực sự có thể là giải pháp tốt hơn.


34
Điều tương tự cũng có thể xảy ra với Java 7 Object.equals: download.oracle.com/javase/7/docs/api/java/util/
Thomas Jung

3
Nếu tôi đang đọc chính xác, Josh Bloch nói trong Java hiệu quả , Mục 8, rằng bạn không nên sử dụng getClass () trong phương thức equals () của bạn; thay vào đó bạn nên sử dụng instanceof.
Jeff Olson

6
@SeanPatrickFloyd Guava-way không chỉ tạo ra một đối tượng mảng cho các varargs, nó còn chuyển đổi TẤT CẢ các tham số thành các đối tượng. Vì vậy, khi bạn truyền 10 giá trị int cho nó thì bạn sẽ kết thúc với 10 đối tượng Integer và một đối tượng mảng. Giải pháp commons-lang chỉ tạo một đối tượng duy nhất, bất kể bạn thêm bao nhiêu giá trị vào mã băm. Vấn đề tương tự với equals. Quả ổi chuyển đổi tất cả các giá trị thành các đối tượng, commons-lang chỉ tạo ra một đối tượng mới.
kayahr

1
@wonhee Tôi hoàn toàn không đồng ý rằng điều này tốt hơn. Sử dụng Reflection để tính mã băm không phải là điều tôi sẽ làm. Chi phí hoạt động có thể không đáng kể, nhưng nó chỉ cảm thấy sai.
Sean Patrick Floyd

1
@kaushik thực hiện một trận chung kết lớp thực sự giải quyết các vấn đề tiềm ẩn của cả hai phiên bản (instanceof và getClass ()), miễn là bạn chỉ thực hiện bằng () trong các lớp lá
Sean Patrick Floyd

18

Các bạn ơi, dậy đi! Vì Java 7 có các phương thức trợ giúp cho bằnghashCode trong thư viện chuẩn. Cách sử dụng của chúng hoàn toàn tương đương với việc sử dụng các phương pháp ổi.


a) tại thời điểm câu hỏi này được hỏi, Java 7 chưa có b) về mặt kỹ thuật, chúng không hoàn toàn tương đương. jdk có phương thức Object.equals so với phương thức Object.equal của Guava. Tôi chỉ có thể sử dụng nhập khẩu tĩnh với phiên bản của Guava. Đó chỉ là mỹ phẩm, tôi biết, nhưng nó làm cho ổi không đáng chú ý hơn.
Sean Patrick Floyd

Đây không phải là một phương thức tốt để ghi đè một đối tượng bằng phương thức do thực tế là Object.equals sẽ gọi phương thức .equals của thể hiện. Nếu bạn gọi Object.equals trong phương thức .equals của cá thể, nó sẽ dẫn đến tràn ngăn xếp.
dardo

Bạn có thể đưa ra một ví dụ, khi nó rơi vào một vòng lặp?
Mikhail Golubtsov

OP đang yêu cầu ghi đè phương thức equals () trong Object. Theo tài liệu của phương thức tĩnh Object.equals (): "Trả về true nếu các đối số bằng nhau và sai khác. Do đó, nếu cả hai đối số đều null, true được trả về và nếu chính xác một đối số là null, false là được trả về. Mặt khác, đẳng thức được xác định bằng cách sử dụng phương thức bằng của đối số thứ nhất. "Do đó, nếu bạn đã sử dụng Object.equals () trong trường hợp bị ghi đè bằng () thì nó sẽ gọi phương thức bằng chính nó, sau đó Object.equals () sau đó chính nó một lần nữa, cho một ngăn xếp tràn.
dardo

@dardo Chúng ta đang nói về việc thực hiện bình đẳng cấu trúc, vì vậy nó có nghĩa là hai đối tượng bằng nhau nếu các lĩnh vực của họ làm. Xem ví dụ ổi ở trên, cách thực hiện bằng.
Mikhail Golubtsov

8

Nếu bạn không muốn phụ thuộc vào thư viện của bên thứ 3 (có thể bạn đang chạy một thiết bị có tài nguyên hạn chế) và thậm chí bạn không muốn nhập phương thức của riêng mình, bạn cũng có thể để IDE thực hiện công việc, ví dụ như sử dụng nhật thực

Source -> Generate hashCode() and equals()...

Bạn sẽ nhận được mã 'gốc' mà bạn có thể định cấu hình tùy thích và bạn phải hỗ trợ các thay đổi.


Ví dụ (nhật thực Juno):

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

public class FooBar {

    public String string;
    public List<String> stringList;
    public String[] stringArray;

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((string == null) ? 0 : string.hashCode());
        result = prime * result + Arrays.hashCode(stringArray);
        result = prime * result
                + ((stringList == null) ? 0 : stringList.hashCode());
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        FooBar other = (FooBar) obj;
        if (string == null) {
            if (other.string != null)
                return false;
        } else if (!string.equals(other.string))
            return false;
        if (!Arrays.equals(stringArray, other.stringArray))
            return false;
        if (stringList == null) {
            if (other.stringList != null)
                return false;
        } else if (!stringList.equals(other.stringList))
            return false;
        return true;
    }

}

14
Đúng, nhưng mã được tạo bởi Eclipse là không thể đọc được và không thể nhầm lẫn.
Sean Patrick Floyd

6
Xin vui lòng, đừng bao giờ nghĩ về một cái gì đó khủng khiếp như nhật thực tạo ra equals. Nếu bạn không muốn phụ thuộc vào thư viện của bên thứ 3, thì hãy viết phương thức một dòng như Objects.equalmình. Ngay cả khi chỉ được sử dụng một hoặc hai lần, nó làm cho cách mã tốt hơn!
maaartinus

@maaartinus equals/ hashCodephương pháp một dòng ???
FrVaBe

1
@maaartinus Guava là thư viện của bên thứ 3. Tôi đã chỉ ra rằng giải pháp của tôi có thể được sử dụng nếu bạn muốn TRÁNH bằng thư viện của bên thứ 3.
FrVaBe

1
@FrVaBe: Và tôi đã viết "Nếu bạn không muốn phụ thuộc vào thư viện của bên thứ 3, thì hãy tự viết phương thức một dòng như Object.equal." Và sau đó tôi đã viết phương pháp một dòng mà bạn có thể sử dụng để TRÁNH bằng cách sử dụng Guava và vẫn cắt độ dài bằng khoảng một nửa.
maaartinus

6

EqualsBuilder và HashCodeBuilder có hai khía cạnh chính khác với mã được viết thủ công:

  • xử lý null
  • tạo cá thể

EqualsBuilder và HashCodeBuilder giúp dễ dàng so sánh các trường có thể là null. Với mã viết tay thủ công, điều này tạo ra rất nhiều mẫu soạn sẵn.

Mặt khác, EqualsBuilder sẽ tạo một thể hiện cho mỗi lệnh gọi phương thức bằng. Nếu các phương thức bằng của bạn được gọi thường xuyên, điều này sẽ tạo ra rất nhiều trường hợp.

Đối với Hibernate, việc thực hiện bằng và hashCode không có sự khác biệt. Họ chỉ là một chi tiết thực hiện. Đối với hầu hết tất cả các đối tượng miền được tải với chế độ ngủ đông, có thể bỏ qua chi phí thời gian chạy (ngay cả khi không có phân tích thoát) của Trình tạo . Cơ sở dữ liệu và truyền thông trên sẽ có ý nghĩa.

Như skaffman đã đề cập, phiên bản phản chiếu không thể được sử dụng trong mã sản xuất. Sự phản ánh sẽ bị chậm lại và "việc thực hiện" sẽ không đúng với tất cả các lớp trừ những lớp đơn giản nhất. Đưa tất cả các thành viên vào tài khoản cũng nguy hiểm vì các thành viên mới được giới thiệu thay đổi hành vi phương thức bằng. Phiên bản phản chiếu có thể hữu ích trong mã thử nghiệm.


Tôi không đồng ý rằng việc thực hiện phản ánh "sẽ không đúng cho tất cả trừ các lớp đơn giản nhất." Với các trình xây dựng, bạn có thể loại trừ rõ ràng các trường nếu bạn muốn, vì vậy việc triển khai thực sự phụ thuộc vào định nghĩa khóa doanh nghiệp của bạn. Thật không may, tôi không thể không đồng ý với khía cạnh hiệu suất của việc thực hiện dựa trên phản ánh.
Digitaljoel

1
@digitaljoel Có, bạn có thể loại trừ các trường, nhưng các định nghĩa này không tái cấu trúc lưu. Vì vậy, tôi đã không đề cập đến chúng trên mục đích.
Thomas Jung


0

Nếu bạn chỉ đang xử lý bean thực thể trong đó id là khóa chính, bạn có thể đơn giản hóa.

   @Override
   public boolean equals(Object other)
   {
      if (this == other) { return true; }
      if ((other == null) || (other.getClass() != this.getClass())) { return false; }

      EntityBean castOther = (EntityBean) other;
      return new EqualsBuilder().append(this.getId(), castOther.getId()).isEquals();
   }

0

Theo tôi, nó không chơi tốt với Hibernate, đặc biệt là các ví dụ từ câu trả lời so sánh độ dài, tên và con cho một số thực thể. Hibernate khuyên nên sử dụng khóa kinh doanh để được sử dụng trong bằng () và hashCode () và họ có lý do của họ. Nếu bạn sử dụng trình tạo tự động bằng () và hàm hashCode () trên khóa doanh nghiệp của mình, thì không sao, chỉ cần xem xét các vấn đề về hiệu suất như đã đề cập trước đây. Nhưng mọi người thường sử dụng tất cả các thuộc tính IMO rất sai. Ví dụ: Tôi hiện đang làm việc trong dự án nơi các thực thể được viết bằng Pojomatic với @AutoProperty, những gì tôi cho là một mẫu thực sự xấu.

Hai kịch bản chính của chúng để sử dụng hashCode () và bằng () là:

  • khi bạn đặt các thể hiện của các lớp liên tục vào Tập hợp (cách được đề xuất để thể hiện các liên kết có giá trị) và
  • khi bạn sử dụng gắn lại các trường hợp tách rời

Vì vậy, hãy giả sử thực thể của chúng ta trông như thế này:

class Entity {
  protected Long id;
  protected String someProp;
  public Entity(Long id, String someProp);
}

Entity entity1 = new Entity(1, "a");
Entity entity2 = new Entity(1, "b");

Cả hai đều là cùng một thực thể cho Hibernate, đã được tìm nạp từ một số phiên tại một số điểm (id và lớp / bảng của chúng bằng nhau). Nhưng khi chúng ta triển khai auto bằng () một hashCode () trên tất cả các đạo cụ, chúng ta có gì?

  1. Khi bạn đặt thực thể2 vào tập liên tục nơi thực thể1 đã tồn tại, điều này sẽ được đặt hai lần và sẽ dẫn đến ngoại lệ trong quá trình cam kết.
  2. Nếu bạn muốn đính kèm thực thể tách rời 2 vào phiên, nơi thực thể1 đã tồn tại thì chúng (có lẽ, tôi đã không kiểm tra điều này đặc biệt) sẽ không được hợp nhất đúng cách.

Vì vậy, đối với dự án 99% mà tôi thực hiện, chúng tôi sử dụng cách triển khai bằng () và hashCode () sau đây được viết một lần trong lớp thực thể cơ sở, phù hợp với các khái niệm Hibernate:

@Override
public boolean equals(Object obj) {
    if (StringUtils.isEmpty(id))
        return super.equals(obj);

    return getClass().isInstance(obj) && id.equals(((IDomain) obj).getId());
}

@Override
public int hashCode() {
    return StringUtils.isEmpty(id)
        ? super.hashCode()
        : String.format("%s/%s", getClass().getSimpleName(), getId()).hashCode();
}

Đối với thực thể nhất thời, tôi làm tương tự những gì Hibernate sẽ làm trên bước kiên trì, tức là. Tôi sử dụng ví dụ phù hợp. Đối với các đối tượng liên tục, tôi so sánh khóa duy nhất, đó là bảng / id (tôi không bao giờ sử dụng khóa tổng hợp).


0

Chỉ trong trường hợp, những người khác sẽ thấy nó hữu ích, tôi đã đến với lớp Trình trợ giúp này để tính toán mã băm để tránh chi phí tạo đối tượng bổ sung được đề cập ở trên (trên thực tế, chi phí của phương thức Object.hash () thậm chí còn lớn hơn khi bạn có kế thừa vì nó sẽ tạo ra một mảng mới trên mỗi cấp độ!).

Ví dụ sử dụng:

public int hashCode() {
    return HashCode.hash(HashCode.hash(timestampMillis), name, dateOfBirth); // timestampMillis is long
}

public int hashCode() {
    return HashCode.hash(super.hashCode(), occupation, children);
}

Trình trợ giúp HashCode:

public class HashCode {

    public static int hash(Object o1, Object o2) {
        return add(Objects.hashCode(o1), o2);
    }

    public static int hash(Object o1, Object o2, Object o3) {
        return hash(Objects.hashCode(o1), o2, o3);
    }

    ...

    public static int hash(Object o1, Object o2, ..., Object o10) {
        return hash(Objects.hashCode(o1), o2, o3, ..., o10);
    }

    public static int hash(int initial, Object o1, Object o2) {
        return add(add(initial, o1), o2);
    }

    ...

    public static int hash(int initial, Object o1, Object o2, ... Object o10) {
        return add(... add(add(add(initial, o1), o2), o3) ..., o10);
    }

    public static int hash(long value) {
        return (int) (value ^ (value >>> 32));
    }

    public static int hash(int initial, long value) {
        return add(initial, hash(value));
    }

    private static int add(int accumulator, Object o) {
        return 31 * accumulator + Objects.hashCode(o);
    }
}

Tôi đã hình dung rằng 10 là số lượng thuộc tính hợp lý tối đa trong mô hình miền, nếu bạn có nhiều hơn, bạn nên nghĩ đến việc tái cấu trúc và giới thiệu nhiều lớp hơn thay vì duy trì một đống Chuỗi và nguyên thủy.

Hạn chế là: nó không hữu ích nếu bạn chủ yếu có các nguyên hàm và / hoặc mảng mà bạn cần băm sâu. (Thông thường đây là trường hợp khi bạn phải xử lý các đối tượng phẳng (chuyển giao) nằm ngoài tầm kiểm soát của bạn).

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.