Java Class.cast () so với toán tử ép kiểu


107

Được dạy trong những ngày học C ++ của tôi về các tệ nạn của toán tử ép kiểu C, ban đầu tôi rất vui khi thấy rằng trong Java 5 java.lang.Classđã có được một castphương thức.

Tôi nghĩ rằng cuối cùng thì chúng ta cũng có một cách xử lý tốt cho việc tuyển chọn.

Hóa ra Class.castkhông giống như static_casttrong C ++. Nó giống hơn reinterpret_cast. Nó sẽ không tạo ra lỗi biên dịch mà nó được mong đợi và thay vào đó sẽ trì hoãn thời gian chạy. Đây là một trường hợp thử nghiệm đơn giản để chứng minh các hành vi khác nhau.

package test;

import static org.junit.Assert.assertTrue;

import org.junit.Test;


public class TestCast
{
    static final class Foo
    {
    }

    static class Bar
    {
    }

    static final class BarSubclass
        extends Bar
    {
    }

    @Test
    public void test ( )
    {
        final Foo foo = new Foo( );
        final Bar bar = new Bar( );
        final BarSubclass bar_subclass = new BarSubclass( );

        {
            final Bar bar_ref = bar;
        }

        {
            // Compilation error
            final Bar bar_ref = foo;
        }
        {
            // Compilation error
            final Bar bar_ref = (Bar) foo;
        }

        try
        {
            // !!! Compiles fine, runtime exception
            Bar.class.cast( foo );
        }
        catch ( final ClassCastException ex )
        {
            assertTrue( true );
        }

        {
            final Bar bar_ref = bar_subclass;
        }

        try
        {
            // Compiles fine, runtime exception, equivalent of C++ dynamic_cast
            final BarSubclass bar_subclass_ref = (BarSubclass) bar;
        }
        catch ( final ClassCastException ex )
        {
            assertTrue( true );
        }
    }
}

Vì vậy, đây là những câu hỏi của tôi.

  1. Có nên Class.cast()bị trục xuất đến vùng đất Generics? Ở đó nó có khá nhiều cách sử dụng hợp pháp.
  2. Trình biên dịch có nên tạo ra lỗi biên dịch khi Class.cast()được sử dụng và các điều kiện bất hợp pháp có thể được xác định tại thời điểm biên dịch không?
  3. Java có nên cung cấp toán tử ép kiểu như một cấu trúc ngôn ngữ tương tự như C ++ không?

4
Câu trả lời đơn giản: (1) "Đất của Generics" ở đâu? Điều đó khác với cách sử dụng toán tử ép kiểu bây giờ như thế nào? (2) Có thể. Nhưng trong 99% tất cả các mã Java đã từng được viết, rất khó cho bất kỳ ai sử dụng Class.cast()khi các điều kiện bất hợp pháp có thể được xác định tại thời điểm biên dịch. Trong trường hợp đó, tất cả mọi người, trừ bạn chỉ sử dụng toán tử truyền chuẩn. (3) Java có một toán tử ép kiểu như một cấu trúc ngôn ngữ. Nó không tương tự như C ++. Đó là vì nhiều cấu trúc ngôn ngữ của Java không giống với C ++. Mặc dù có những điểm tương đồng bề ngoài, Java và C ++ khá khác nhau.
Daniel Pryden

Câu trả lời:


117

Tôi chỉ sử dụng Class.cast(Object)để tránh các cảnh báo trong "đất đai chung chung". Tôi thường thấy các phương pháp làm những việc như thế này:

@SuppressWarnings("unchecked")
<T> T doSomething() {
    Object o;
    // snip
    return (T) o;
}

Tốt nhất bạn nên thay thế bằng:

<T> T doSomething(Class<T> cls) {
    Object o;
    // snip
    return cls.cast(o);
}

Đó là trường hợp sử dụng duy nhất Class.cast(Object)mà tôi từng xem qua.

Về cảnh báo trình biên dịch: Tôi nghi ngờ điều đó Class.cast(Object)không đặc biệt đối với trình biên dịch. Nó có thể được tối ưu hóa khi được sử dụng tĩnh (tức Foo.class.cast(o)là hơn cls.cast(o)) nhưng tôi chưa từng thấy ai sử dụng nó - điều này khiến nỗ lực xây dựng tối ưu hóa này vào trình biên dịch hơi vô ích.


8
Tôi nghĩ cách chính xác trong trường hợp này là thực hiện một trường hợp kiểm tra đầu tiên như sau: if (cls.isInstance (o)) {return cls.cast (o); } Tất nhiên là ngoại trừ nếu bạn chắc chắn loại sẽ đúng.
Puce

1
Tại sao biến thể thứ hai tốt hơn? Không phải biến thể đầu tiên hiệu quả hơn vì mã của người gọi vẫn sẽ thực hiện truyền động?
user1944408 Ngày

1
@ user1944408, miễn là bạn nói đúng về hiệu suất thì có thể được, mặc dù khá nhiều không đáng kể. Mặc dù vậy, tôi không thích ý tưởng nhận ClassCastExceptions khi không có trường hợp rõ ràng. Thêm vào đó, điều thứ hai hoạt động tốt hơn nơi trình biên dịch không thể suy luận T, ví dụ như list.add (điều này <String> doSomething ().) Vs list.add (doSomething (String.class))
sfussenegger

2
Nhưng bạn vẫn có thể nhận được ClassCastExceptions khi bạn gọi cls.cast (o) trong khi bạn không nhận được bất kỳ cảnh báo nào trong thời gian biên dịch. Tôi thích biến thể đầu tiên hơn nhưng tôi sẽ sử dụng biến thể thứ hai khi tôi cần ví dụ trả về null nếu tôi không thể thực hiện truyền. Trong trường hợp đó, tôi sẽ bọc cls.cast (o) trong một khối try catch. Tôi cũng đồng ý với bạn rằng điều này cũng tốt hơn khi trình biên dịch không thể suy ra T. Cảm ơn đã trả lời.
user1944408 Ngày

1
Tôi cũng sử dụng biến thể thứ hai khi tôi cần Class <T> cls là động, ví dụ: nếu tôi sử dụng phản chiếu nhưng trong tất cả các trường hợp khác, tôi thích biến thể đầu tiên hơn. Tôi đoán đó chỉ là sở thích cá nhân.
user1944408 Ngày

20

Đầu tiên, bạn rất không khuyến khích thực hiện hầu hết mọi loại cast, vì vậy bạn nên hạn chế nó càng nhiều càng tốt! Bạn mất đi những lợi ích của các tính năng được đánh máy mạnh trong thời gian biên dịch của Java.

Trong mọi trường hợp, Class.cast()nên được sử dụng chủ yếu khi bạn lấy Classmã thông báo qua phản chiếu. Viết thành ngữ hơn

MyObject myObject = (MyObject) object

hơn là

MyObject myObject = MyObject.class.cast(object)

CHỈNH SỬA: Lỗi tại thời điểm biên dịch

Trên tất cả, Java chỉ thực hiện kiểm tra truyền vào thời gian chạy. Tuy nhiên, trình biên dịch có thể đưa ra lỗi nếu nó có thể chứng minh rằng các lần ép kiểu đó không bao giờ có thể thành công (ví dụ: truyền một lớp sang một lớp khác không phải là siêu kiểu và ép kiểu lớp cuối cùng đến lớp / giao diện không nằm trong hệ thống phân cấp kiểu của nó). Ở đây vì FooBarlà các lớp không nằm trong hệ thống phân cấp khác, nên việc truyền diễn viên không bao giờ có thể thành công.


Tôi hoàn toàn đồng ý rằng phôi nên được sử dụng một cách tiết kiệm, đó là lý do tại sao có thứ gì đó có thể dễ dàng tìm kiếm sẽ mang lại lợi ích lớn cho việc tái cấu trúc / duy trì mã. Nhưng vấn đề là mặc dù thoạt nhìn Class.castcó vẻ phù hợp với dự luật nhưng nó lại tạo ra nhiều vấn đề hơn là giải quyết được.
Alexander Pogrebnyak

16

Việc thử dịch các cấu trúc và khái niệm giữa các ngôn ngữ luôn có vấn đề và thường gây hiểu lầm. Casting cũng không ngoại lệ. Đặc biệt bởi vì Java là một ngôn ngữ động và C ++ có phần khác biệt.

Tất cả quá trình truyền trong Java, bất kể bạn làm như thế nào, đều được thực hiện trong thời gian chạy. Thông tin loại được giữ trong thời gian chạy. C ++ là một hỗn hợp nhiều hơn một chút. Bạn có thể truyền một cấu trúc trong C ++ sang một cấu trúc khác và nó chỉ đơn thuần là sự diễn giải lại các byte đại diện cho các cấu trúc đó. Java không hoạt động theo cách đó.

Ngoài ra, generic trong Java và C ++ rất khác nhau. Đừng quan tâm quá mức đến cách bạn làm những việc C ++ trong Java. Bạn cần học cách làm mọi thứ theo cách Java.


Không phải tất cả thông tin đều được lưu giữ trong thời gian chạy. Như bạn có thể thấy từ ví dụ của tôi, (Bar) foonó tạo ra lỗi tại thời điểm biên dịch, nhưng Bar.class.cast(foo)không. Theo tôi nếu nó được sử dụng theo cách này thì nó nên.
Alexander Pogrebnyak

5
@Alexander Pogrebnyak: Đừng làm vậy! Bar.class.cast(foo)nói rõ ràng với trình biên dịch rằng bạn muốn thực hiện ép kiểu trong thời gian chạy. Nếu bạn muốn kiểm tra thời gian biên dịch về tính hợp lệ của quá trình truyền, lựa chọn duy nhất của bạn là thực hiện truyền (Bar) fookiểu.
Daniel Pryden

Bạn nghĩ cách làm giống cách làm là gì? vì java không hỗ trợ kế thừa nhiều lớp.
Yamur

13

Class.cast()hiếm khi được sử dụng trong mã Java. Nếu nó được sử dụng thì thường với các kiểu chỉ được biết trong thời gian chạy (tức là thông qua các Classđối tượng tương ứng của chúng và bởi một số tham số kiểu). Nó chỉ thực sự hữu ích trong mã sử dụng generic (đó cũng là lý do nó không được giới thiệu trước đó).

không tương tự như reinterpret_cast, bởi vì nó sẽ không cho phép bạn phá vỡ hệ thống kiểu trong thời gian chạy hơn bất kỳ kiểu đúc thông thường nào (nghĩa là bạn có thể phá vỡ các tham số kiểu chung, nhưng không thể phá vỡ các kiểu "thực").

Các tệ nạn của toán tử ép kiểu C thường không áp dụng cho Java. Mã Java trông giống kiểu ép kiểu C gần giống nhất dynamic_cast<>()với mã có kiểu tham chiếu trong Java (hãy nhớ: Java có thông tin kiểu thời gian chạy).

Nói chung, so sánh các toán tử ép kiểu C ++ với việc ép kiểu Java là khá khó vì trong Java, bạn chỉ có thể truyền tham chiếu và không có chuyển đổi nào xảy ra với các đối tượng (chỉ có thể chuyển đổi các giá trị nguyên thủy bằng cú pháp này).


dynamic_cast<>()với một loại tham chiếu.
Tom Hawtin - tackline 12/10/09

@Tom: sửa vậy có đúng không? My C ++ là rất gỉ, tôi đã phải tái google rất nhiều của nó ;-)
Joachim Sauer

+1 cho: "Tệ nạn của toán tử ép kiểu C thường không áp dụng cho Java." Hơi đúng. Tôi chỉ định đăng những từ chính xác đó như một bình luận về câu hỏi.
Daniel Pryden

4

Nói chung, toán tử ép kiểu được ưa thích hơn phương thức ép kiểu Class # vì nó ngắn gọn hơn và có thể được trình biên dịch phân tích để tìm ra các vấn đề trắng trợn với mã.

Lớp # cast chịu trách nhiệm kiểm tra kiểu tại thời điểm chạy hơn là trong quá trình biên dịch.

Chắc chắn có các trường hợp sử dụng cho lớp # ép kiểu, đặc biệt là khi nói đến các hoạt động phản chiếu.

Vì lambda's đến với java, cá nhân tôi thích sử dụng Class # cast với API collection / stream nếu tôi đang làm việc với các kiểu trừu tượng chẳng hạn.

Dog findMyDog(String name, Breed breed) {
    return lostAnimals.stream()
                      .filter(Dog.class::isInstance)
                      .map(Dog.class::cast)
                      .filter(dog -> dog.getName().equalsIgnoreCase(name))
                      .filter(dog -> dog.getBreed() == breed)
                      .findFirst()
                      .orElse(null);
}

3

C ++ và Java là các ngôn ngữ khác nhau.

Toán tử ép kiểu Java C bị hạn chế hơn nhiều so với phiên bản C / C ++. Về hiệu quả, việc ép kiểu Java giống như C ++ dynamic_cast nếu đối tượng bạn có không thể được truyền sang lớp mới, bạn sẽ nhận được thời gian chạy (hoặc nếu có đủ thông tin trong mã thì thời gian biên dịch) ngoại lệ. Vì vậy, ý tưởng C ++ không sử dụng phôi kiểu C không phải là một ý tưởng hay trong Java


0

Ngoài việc loại bỏ các cảnh báo truyền xấu như được đề cập nhiều nhất, Class.cast là truyền trong thời gian chạy chủ yếu được sử dụng với truyền chung, do thông tin chung sẽ bị xóa trong thời gian chạy và một số cách mỗi chung sẽ được coi là Đối tượng, điều này dẫn đến việc không ném một ClassCastException sớm.

ví dụ serviceLoder sử dụng thủ thuật này khi tạo các đối tượng, kiểm tra S p = service.cast (c.newInstance ()); điều này sẽ ném ra một ngoại lệ ép kiểu lớp khi SP = (S) c.newInstance (); sẽ không và có thể hiển thị cảnh báo 'An toàn kiểu: Bỏ chọn truyền từ Đối tượng sang S' . (giống như Đối tượng P = (Đối tượng) c.newInstance ();)

- chỉ đơn giản là nó kiểm tra xem đối tượng ép kiểu có phải là cá thể của lớp đúc không, sau đó nó sẽ sử dụng toán tử ép kiểu để ép kiểu và ẩn cảnh báo bằng cách chặn nó.

triển khai java cho diễn viên động:

@SuppressWarnings("unchecked")
public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
        throw new ClassCastException(cannotCastMsg(obj));
    return (T) obj;
}




    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }

0

Cá nhân tôi đã sử dụng điều này trước đây để xây dựng bộ chuyển đổi JSON sang POJO. Trong trường hợp JSONObject được xử lý bằng hàm chứa một mảng hoặc các JSONObject lồng nhau (ngụ ý rằng dữ liệu ở đây không thuộc kiểu nguyên thủy hoặc String), tôi cố gắng gọi phương thức setter bằng cách sử dụng class.cast()theo cách này:

public static Object convertResponse(Class<?> clazz, JSONObject readResultObject) {
    ...
    for(Method m : clazz.getMethods()) {
        if(!m.isAnnotationPresent(convertResultIgnore.class) && 
            m.getName().toLowerCase().startsWith("set")) {
        ...
        m.invoke(returnObject,  m.getParameters()[0].getClass().cast(convertResponse(m.getParameters()[0].getType(), readResultObject.getJSONObject(key))));
    }
    ...
}

Không chắc liệu điều này có cực kỳ hữu ích hay không, nhưng như đã nói ở đây trước đây, phản chiếu là một trong số rất ít trường hợp sử dụng hợp pháp class.cast()mà tôi có thể nghĩ đến, ít nhất bạn có một ví dụ khác bây giờ.

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.