Tránh instanceof trong Java


102

Có một chuỗi các hoạt động "instanceof" được coi là một "mùi mã". Câu trả lời tiêu chuẩn là "sử dụng tính đa hình". Tôi sẽ làm như thế nào trong trường hợp này?

Có một số lớp con của một lớp cơ sở; không ai trong số họ nằm trong tầm kiểm soát của tôi. Một tình huống tương tự sẽ xảy ra với các lớp Java Integer, Double, BigDecimal, v.v.

if (obj instanceof Integer) {NumberStuff.handle((Integer)obj);}
else if (obj instanceof BigDecimal) {BigDecimalStuff.handle((BigDecimal)obj);}
else if (obj instanceof Double) {DoubleStuff.handle((Double)obj);}

Tôi có quyền kiểm soát NumberStuff, v.v.

Tôi không muốn sử dụng nhiều dòng mã trong đó một vài dòng sẽ làm được. (Đôi khi tôi tạo một HashMap ánh xạ Integer.class đến một phiên bản của IntegerStuff, BigDecimal.class đến một phiên bản BigDecimalStuff, v.v. Nhưng hôm nay tôi muốn một cái gì đó đơn giản hơn.)

Tôi muốn một cái gì đó đơn giản như sau:

public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }

Nhưng Java không hoạt động theo cách đó.

Tôi muốn sử dụng các phương thức tĩnh khi định dạng. Những thứ tôi đang định dạng là hỗn hợp, trong đó Thing1 có thể chứa một mảng Thing2 và một Thing2 có thể chứa một mảng Thing1. Tôi đã gặp sự cố khi triển khai các bộ định dạng của mình như sau:

class Thing1Formatter {
  private static Thing2Formatter thing2Formatter = new Thing2Formatter();
  public format(Thing thing) {
      thing2Formatter.format(thing.innerThing2);
  }
}
class Thing2Formatter {
  private static Thing1Formatter thing1Formatter = new Thing1Formatter();
  public format(Thing2 thing) {
      thing1Formatter.format(thing.innerThing1);
  }
}

Vâng, tôi biết HashMap và một đoạn mã khác cũng có thể khắc phục điều đó. Nhưng "instanceof" dường như rất dễ đọc và có thể bảo trì bằng cách so sánh. Có món gì đơn giản mà không nặng mùi?

Ghi chú thêm vào 5/10/2010:

Hóa ra là các lớp con mới có thể sẽ được thêm vào trong tương lai và mã hiện tại của tôi sẽ phải xử lý chúng một cách khéo léo. HashMap trên Class sẽ không hoạt động trong trường hợp đó vì sẽ không tìm thấy Class. Một chuỗi câu lệnh if, bắt đầu bằng câu cụ thể nhất và kết thúc bằng câu chung chung nhất, có lẽ là tốt nhất sau tất cả:

if (obj instanceof SubClass1) {
    // Handle all the methods and properties of SubClass1
} else if (obj instanceof SubClass2) {
    // Handle all the methods and properties of SubClass2
} else if (obj instanceof Interface3) {
    // Unknown class but it implements Interface3
    // so handle those methods and properties
} else if (obj instanceof Interface4) {
    // likewise.  May want to also handle case of
    // object that implements both interfaces.
} else {
    // New (unknown) subclass; do what I can with the base class
}

4
Tôi muốn đề xuất một [kiểu khách truy cập] [1]. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore

25
Mẫu khách truy cập yêu cầu thêm một phương thức vào lớp đích (Ví dụ: Số nguyên) - dễ trong JavaScript, khó trong Java. Mẫu tuyệt vời khi thiết kế các lớp mục tiêu; không dễ dàng như vậy khi cố gắng dạy một lớp cũ các thủ thuật mới.
Mark Lutton

4
@lexicore: đánh dấu trong nhận xét bị hạn chế. Sử dụng [text](link)để đăng liên kết trong bình luận.
BalusC

2
"Nhưng Java không hoạt động theo cách đó." Có lẽ tôi đang hiểu sai mọi thứ, nhưng Java hỗ trợ nạp chồng phương thức (ngay cả trên các phương thức tĩnh) rất tốt ... chỉ là các phương thức của bạn ở trên thiếu kiểu trả về.
Powerlord

4
@Powerlord Độ phân giải quá tải là tĩnh tại thời gian biên dịch .
Aleksandr Dubinsky

Câu trả lời:


55

Bạn có thể quan tâm đến mục này từ blog Amazon của Steve Yegge: "khi đa hình không thành công" . Về cơ bản, anh ấy đang giải quyết những trường hợp như thế này, khi tính đa hình gây ra nhiều rắc rối hơn là nó giải quyết được.

Vấn đề là để sử dụng tính đa hình, bạn phải tạo logic của phần "xử lý" của mỗi lớp 'chuyển đổi' - tức là Số nguyên, v.v. trong trường hợp này. Rõ ràng điều này không thực tế. Đôi khi nó thậm chí không phải là nơi thích hợp để đặt mã về mặt logic. Ông khuyến nghị phương pháp tiếp cận 'ví dụ' để giảm thiểu một số tệ nạn.

Như với tất cả các trường hợp bạn buộc phải viết mã có mùi, hãy giữ cho nó được cài trong một phương pháp (hoặc nhiều nhất là một lớp) để mùi không bị thoát ra ngoài.


22
Đa hình không thất bại. Thay vào đó, Steve Yegge đã thất bại trong việc phát minh ra kiểu Visitor, là sự thay thế hoàn hảo cho instanceof.
Rotsor

12
Tôi không thấy khách truy cập giúp đỡ ở đây như thế nào. Vấn đề là phản hồi của OpinionatedElf đối với NewMonster không nên được mã hóa trong NewMonster, mà trong OpinionatedElf.
DJClayworth

2
Điểm của ví dụ là OpinionatedElf không thể biết từ dữ liệu có sẵn là nó thích hay không thích Monster. Nó phải biết Quái vật thuộc Class gì. Điều đó yêu cầu một instanceof, hoặc Monster phải biết bằng cách nào đó liệu OpinionatedElf có thích nó hay không. Khách truy cập không nhận được vòng đó.
DJClayworth

2
Mẫu @DJClayworth khách không được xung quanh đó bằng cách thêm một phương pháp để Monsterlớp, trách nhiệm trong số đó là về cơ bản để giới thiệu các đối tượng, như "Xin chào, tôi là một Orc. Bạn nghĩ gì về tôi?". Yêu tinh có ý kiến ​​sau đó có thể đánh giá quái vật dựa trên những "lời chào" này, với một mã tương tự như bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. Sau đó class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }, mã duy nhất dành riêng cho quái vật sẽ là , đủ cho mỗi lần kiểm tra quái vật một lần và mãi mãi.
Rotsor

2
Hãy xem câu trả lời của @Chris Knight để biết lý do tại sao trong một số trường hợp, khách không thể áp dụng được tính năng Truy cập.
James P.

20

Như đã nhấn mạnh trong các nhận xét, mẫu khách truy cập sẽ là một lựa chọn tốt. Nhưng nếu không có sự kiểm soát trực tiếp đối với target / acceptor / visitee, bạn không thể triển khai mô hình đó. Đây là một cách mà mẫu khách truy cập có thể vẫn được sử dụng ở đây mặc dù bạn không có quyền kiểm soát trực tiếp đối với các lớp con bằng cách sử dụng trình bao bọc (lấy Số nguyên làm ví dụ):

public class IntegerWrapper {
    private Integer integer;
    public IntegerWrapper(Integer anInteger){
        integer = anInteger;
    }
    //Access the integer directly such as
    public Integer getInteger() { return integer; }
    //or method passthrough...
    public int intValue() { return integer.intValue(); }
    //then implement your visitor:
    public void accept(NumericVisitor visitor) {
        visitor.visit(this);
    }
}

Tất nhiên, gói một lớp cuối cùng có thể được coi là mùi của riêng nó nhưng có thể nó rất phù hợp với các lớp con của bạn. Cá nhân tôi không nghĩ instanceofrằng có mùi hôi ở đây, đặc biệt là nếu nó được giới hạn trong một phương pháp và tôi rất vui khi sử dụng nó (có thể là theo gợi ý của riêng tôi ở trên). Như bạn nói, nó khá dễ đọc, an toàn và có thể bảo trì. Như mọi khi, hãy giữ nó đơn giản.


Có, "Định dạng", "Kết hợp", "Các loại khác nhau" tất cả các đường nối để trỏ theo hướng của khách truy cập.
Thomas Ahle

3
làm thế nào để bạn xác định trình bao bọc nào bạn sẽ sử dụng? thông qua if instanceof phân nhánh?
răng nhanh

2
Như @fasttooth chỉ ra giải pháp này chỉ thay đổi vấn đề. Thay vì sử dụng instanceofđể gọi đúng handle()phương pháp bây giờ bạn sẽ phải sử dụng nó gọi quyền XWrapperconstructor ...
Matthias

15

Thay vì rất lớn if, bạn có thể đặt các cá thể mà bạn xử lý trong một bản đồ (key: class, value: handler).

Nếu việc tra cứu theo khóa trả về null, hãy gọi một phương thức xử lý đặc biệt để cố gắng tìm một trình xử lý phù hợp (ví dụ bằng cách gọi isInstance()trên mọi khóa trong bản đồ).

Khi tìm thấy một trình xử lý, hãy đăng ký nó theo khóa mới.

Điều này làm cho trường hợp chung nhanh chóng và đơn giản và cho phép bạn xử lý kế thừa.


+1 Tôi đã sử dụng cách tiếp cận này khi xử lý mã được tạo từ lược đồ XML hoặc hệ thống nhắn tin, nơi có hàng tá loại đối tượng, được chuyển đến mã của tôi theo cách không an toàn về cơ bản.
DNA

13

Bạn có thể sử dụng phản chiếu:

public final class Handler {
  public static void handle(Object o) {
    try {
      Method handler = Handler.class.getMethod("handle", o.getClass());
      handler.invoke(null, o);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public static void handle(Integer num) { /* ... */ }
  public static void handle(BigDecimal num) { /* ... */ }
  // to handle new types, just add more handle methods...
}

Bạn có thể mở rộng ý tưởng để xử lý chung các lớp con và lớp triển khai các giao diện nhất định.


34
Tôi sẽ tranh luận rằng điều này thậm chí còn có mùi hơn so với toán tử instanceof. Nên làm việc mặc dù.
Tim Büthe

5
@Tim Büthe: Ít nhất bạn không phải đối phó với một if then elsechuỗi đang phát triển để thêm, xóa hoặc sửa đổi trình xử lý. Mã ít dễ bị thay đổi hơn. Vì vậy, tôi muốn nói rằng vì lý do này, nó vượt trội hơn so với instanceofcách tiếp cận. Dù sao, tôi chỉ muốn đưa ra một giải pháp thay thế hợp lệ.
Jordão

1
Đây thực chất là như thế nào một ngôn ngữ năng động sẽ xử lý tình hình, qua gõ vịt
DNA

@DNA: đó không phải là multimethods ?
Jordão

1
Tại sao bạn lặp lại tất cả các phương pháp thay vì sử dụng getMethod(String name, Class<?>... parameterTypes)? Hoặc nếu không, tôi sẽ thay thế ==bằng isAssignableFromđể kiểm tra kiểu của tham số.
Aleksandr Dubinsky

9

Bạn có thể xem xét mô hình Chuỗi trách nhiệm . Đối với ví dụ đầu tiên của bạn, một cái gì đó như:

public abstract class StuffHandler {
   private StuffHandler next;

   public final boolean handle(Object o) {
      boolean handled = doHandle(o);
      if (handled) { return true; }
      else if (next == null) { return false; }
      else { return next.handle(o); }
   }

   public void setNext(StuffHandler next) { this.next = next; }

   protected abstract boolean doHandle(Object o);
}

public class IntegerHandler extends StuffHandler {
   @Override
   protected boolean doHandle(Object o) {
      if (!o instanceof Integer) {
         return false;
      }
      NumberHandler.handle((Integer) o);
      return true;
   }
}

và sau đó tương tự cho các trình xử lý khác của bạn. Sau đó, đó là trường hợp xâu chuỗi các StuffHandlers lại với nhau theo thứ tự (cụ thể nhất đến cụ thể nhất, với một trình xử lý 'dự phòng' cuối cùng) và mã người gửi của bạn chỉ là firstHandler.handle(o);.

(Một giải pháp thay thế là, thay vì sử dụng chuỗi, chỉ cần có một List<StuffHandler>trong lớp điều phối của bạn và để nó lặp qua danh sách cho đến khi handle()trả về true).


9

Tôi nghĩ rằng giải pháp tốt nhất là HashMap với Class là khóa và Xử lý là giá trị. Lưu ý rằng giải pháp dựa trên HashMap chạy với độ phức tạp thuật toán không đổi θ (1), trong khi chuỗi có mùi của if-instanceof-else chạy ở độ phức tạp thuật toán tuyến tính O (N), trong đó N là số liên kết trong chuỗi if-instanceof-else (tức là số lượng các lớp khác nhau được xử lý). Vì vậy, hiệu suất của giải pháp dựa trên HashMap cao hơn về mặt tiệm cận N lần so với hiệu suất của giải pháp chuỗi if-instanceof-else. Hãy xem xét rằng bạn cần xử lý các lớp con của lớp Message một cách khác nhau: Message1, Message2, v.v. Dưới đây là đoạn mã để xử lý dựa trên HashMap.

public class YourClass {
    private class Handler {
        public void go(Message message) {
            // the default implementation just notifies that it doesn't handle the message
            System.out.println(
                "Possibly due to a typo, empty handler is set to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        }
    }
    private Map<Class<? extends Message>, Handler> messageHandling = 
        new HashMap<Class<? extends Message>, Handler>();

    // Constructor of your class is a place to initialize the message handling mechanism    
    public YourClass() {
        messageHandling.put(Message1.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message1
        } });
        messageHandling.put(Message2.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message2
        } });
        // etc. for Message3, etc.
    }

    // The method in which you receive a variable of base class Message, but you need to
    //   handle it in accordance to of what derived type that instance is
    public handleMessage(Message message) {
        Handler handler = messageHandling.get(message.getClass());
        if (handler == null) {
            System.out.println(
                "Don't know how to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        } else {
            handler.go(message);
        }
    }
}

Thông tin thêm về cách sử dụng các biến kiểu Class trong Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


đối với một số ít trường hợp (có thể cao hơn so với số lượng những lớp dành cho bất kỳ ví dụ thực tế) các if-else sẽ làm tốt hơn bản đồ, bên cạnh không có sử dụng bộ nhớ heap ở tất cả
idelvall


0

Tôi đã giải quyết vấn đề này bằng cách sử dụng reflection(khoảng 15 năm trở lại thời kỳ tiền Generics).

GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();

Tôi đã định nghĩa một Lớp Chung (lớp Cơ sở trừu tượng). Tôi đã xác định nhiều triển khai cụ thể của lớp cơ sở. Mỗi lớp cụ thể sẽ được tải với tham số className. Tên lớp này được định nghĩa là một phần của cấu hình.

Lớp cơ sở xác định trạng thái chung trên tất cả các lớp cụ thể và các lớp cụ thể sẽ sửa đổi trạng thái bằng cách ghi đè các quy tắc trừu tượng được xác định trong lớp cơ sở.

Vào thời điểm đó, tôi không biết tên của cơ chế này, nó đã được gọi là reflection.

Một số lựa chọn thay thế khác được liệt kê trong bài viết này : Mapenumngoài sự phản ánh.


Chỉ tò mò, tại sao bạn không làm cho GenericClassmột interface?
Ztyx

Tôi đã có tình trạng chung và hành vi mặc định, mà phải được chia sẻ trên nhiều đối tượng liên quan
Ravindra babu
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.