Java Class.cast () เทียบกับตัวดำเนินการ cast


107

หลังจากได้รับการสอนในช่วงวัน C ++ ของฉันเกี่ยวกับความชั่วร้ายของตัวดำเนินการแบบ C-style ฉันรู้สึกยินดีในตอนแรกที่พบว่าใน Java 5 java.lang.Classได้รับcastวิธีการ

ฉันคิดว่าในที่สุดเราก็มีวิธีจัดการกับการแคสติ้งแบบ OO

ปรากฎว่าClass.castไม่เหมือนกับstatic_castใน C ++ reinterpret_castมันเป็นเหมือน จะไม่สร้างข้อผิดพลาดในการคอมไพล์ตามที่คาดไว้และจะเลื่อนไปที่รันไทม์แทน นี่คือกรณีทดสอบง่ายๆเพื่อแสดงพฤติกรรมที่แตกต่างกัน

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 );
        }
    }
}

ดังนั้นนี่คือคำถามของฉัน

  1. ควรClass.cast()ถูกเนรเทศไปยังดินแดน Generics หรือไม่? มีการใช้งานที่ถูกต้องตามกฎหมายค่อนข้างน้อย
  2. คอมไพเลอร์ควรสร้างข้อผิดพลาดในการคอมไพล์เมื่อClass.cast()ถูกใช้งานและสามารถกำหนดเงื่อนไขที่ผิดกฎหมายได้ในเวลาคอมไพล์หรือไม่?
  3. Java ควรจัดเตรียมตัวดำเนินการ cast เป็นภาษาที่สร้างคล้ายกับ C ++ หรือไม่

4
คำตอบง่ายๆ: (1) "Generics land" อยู่ที่ไหน? มันแตกต่างจากวิธีการใช้ตัวดำเนินการแคสตอนนี้อย่างไร? (2) น่าจะเป็น แต่ใน 99% ของทุกรหัส Java เคยเขียนมันเป็นอย่างมากไม่น่าสำหรับทุกคนที่จะใช้Class.cast()เมื่อเงื่อนไขที่ผิดกฎหมายสามารถกำหนดเวลาที่รวบรวม ในกรณีนี้ทุกคน แต่คุณใช้แค่ตัวดำเนินการหล่อมาตรฐาน (3) Java มีตัวดำเนินการ cast เป็นโครงสร้างภาษา มันไม่คล้ายกับ C ++ นั่นเป็นเพราะโครงสร้างภาษาของ Java จำนวนมากไม่คล้ายกับ C ++ แม้จะมีความคล้ายคลึงกันเพียงผิวเผิน Java และ C ++ ก็แตกต่างกันมาก
Daniel Pryden

คำตอบ:


117

ฉันเคยใช้Class.cast(Object)เพื่อหลีกเลี่ยงคำเตือนใน "ดินแดนทั่วไป" เท่านั้น ฉันมักจะเห็นวิธีการทำสิ่งต่างๆเช่นนี้:

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

มักจะดีที่สุดที่จะแทนที่โดย:

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

นั่นเป็นกรณีการใช้งานเดียวที่Class.cast(Object)ฉันเคยเจอ

เกี่ยวกับคำเตือนของคอมไพเลอร์: ฉันสงสัยว่านั่นClass.cast(Object)ไม่ใช่เรื่องพิเศษสำหรับคอมไพเลอร์ สามารถปรับให้เหมาะสมเมื่อใช้แบบคงที่ (เช่นFoo.class.cast(o)แทนที่จะเป็นcls.cast(o)) แต่ฉันไม่เคยเห็นใครใช้มัน - ซึ่งทำให้ความพยายามในการสร้างการเพิ่มประสิทธิภาพนี้ลงในคอมไพเลอร์ค่อนข้างไร้ค่า


8
ฉันคิดว่าวิธีที่ถูกต้องในกรณีนี้คือการตรวจสอบเช่นนี้ก่อน: if (cls.isInstance (o)) {return cls.cast (o); } ยกเว้นว่าคุณแน่ใจว่าประเภทถูกต้องแน่นอน
Puce

1
เหตุใดตัวแปรที่สองจึงดีกว่า ตัวแปรแรกไม่มีประสิทธิภาพมากกว่าเพราะรหัสของผู้โทรจะส่งแบบไดนามิกใช่หรือไม่
user1944408

1
@ user1944408 ตราบใดที่คุณพูดอย่างเคร่งครัดเกี่ยวกับประสิทธิภาพก็อาจจะเป็นได้แม้ว่าจะเล็กน้อยมากก็ตาม ฉันไม่ชอบแนวคิดในการรับ ClassCastExceptions ในกรณีที่ไม่มีกรณีที่ชัดเจน นอกจากนี้อันที่สองยังทำงานได้ดีกว่าโดยที่คอมไพเลอร์ไม่สามารถอนุมาน T ได้เช่น list.add (this. <String> doSomething ()) vs. list.add (doSomething (String.class))
sfussenegger

2
แต่คุณยังสามารถรับ ClassCastExceptions ได้เมื่อคุณเรียก cls.cast (o) ในขณะที่คุณไม่ได้รับคำเตือนใด ๆ ในเวลาคอมไพล์ ฉันชอบตัวแปรแรก แต่ฉันจะใช้ตัวแปรที่สองเมื่อฉันต้องการตัวอย่างเช่นเพื่อส่งคืนค่าว่างหากฉันไม่สามารถแคสต์ได้ ในกรณีนั้นฉันจะตัด cls.cast (o) ในบล็อก try catch ฉันเห็นด้วยกับคุณว่าสิ่งนี้ดีกว่าเช่นกันเมื่อคอมไพเลอร์ไม่สามารถอนุมาน T. ขอบคุณสำหรับการตอบกลับ
user1944408

1
ฉันยังใช้ตัวแปรที่สองเมื่อฉันต้องการคลาส <T> cls เป็นไดนามิกเช่นถ้าฉันใช้การสะท้อน แต่ในกรณีอื่น ๆ ฉันชอบตัวแรกมากกว่า ฉันเดาว่านั่นเป็นเพียงรสนิยมส่วนตัว
user1944408

20

อันดับแรกคุณไม่ควรทำเกือบทุกประเภทดังนั้นคุณควร จำกัด การแสดงให้มากที่สุด! คุณจะสูญเสียประโยชน์ของคุณสมบัติที่พิมพ์อย่างหนักในเวลาคอมไพล์ของ Java

ไม่ว่าในกรณีใดClass.cast()ควรใช้เป็นหลักเมื่อคุณดึงClassโทเค็นผ่านการสะท้อนกลับ เป็นสำนวนในการเขียนมากกว่า

MyObject myObject = (MyObject) object

ค่อนข้างมากกว่า

MyObject myObject = MyObject.class.cast(object)

แก้ไข: ข้อผิดพลาดในเวลาคอมไพล์

เหนือสิ่งอื่นใด Java จะทำการตรวจสอบ Cast ในขณะรันเท่านั้น อย่างไรก็ตามคอมไพเลอร์สามารถแสดงข้อผิดพลาดได้หากสามารถพิสูจน์ได้ว่าการแคสต์ดังกล่าวไม่สามารถทำได้สำเร็จ (เช่นแคสคลาสไปยังคลาสอื่นที่ไม่ใช่ซูเปอร์ไทป์และแคสประเภทคลาสสุดท้ายไปยังคลาส / อินเทอร์เฟซที่ไม่ได้อยู่ในลำดับชั้นประเภท) ตั้งแต่นั้นเป็นต้นมาFooและBarเป็นคลาสที่ไม่ได้อยู่ในลำดับชั้นของกันและกันนักแสดงจะไม่มีวันประสบความสำเร็จ


ฉันยอมรับโดยสิ้นเชิงว่าควรใช้การร่ายเท่าที่จำเป็นนั่นเป็นเหตุผลว่าทำไมการมีบางสิ่งที่สามารถค้นหาได้ง่ายจึงเป็นประโยชน์อย่างยิ่งสำหรับการปรับโครงสร้าง / การบำรุงรักษาโค้ด แต่ปัญหาคือแม้ว่าในแวบแรกClass.castดูเหมือนว่าจะพอดีกับใบเรียกเก็บเงิน แต่ก็สร้างปัญหามากกว่าที่จะแก้ได้
Alexander Pogrebnyak

16

เป็นปัญหาเสมอและมักทำให้เข้าใจผิดในการพยายามแปลโครงสร้างและแนวคิดระหว่างภาษา การคัดเลือกนักแสดงไม่มีข้อยกเว้น โดยเฉพาะอย่างยิ่งเนื่องจาก Java เป็นภาษาไดนามิกและ C ++ แตกต่างกันบ้าง

การแคสต์ทั้งหมดใน Java ไม่ว่าคุณจะทำด้วยวิธีใดก็ทำได้ที่รันไทม์ ข้อมูลประเภทถูกเก็บไว้ที่รันไทม์ C ++ เป็นส่วนผสมมากกว่าเล็กน้อย คุณสามารถโยนโครงสร้างใน C ++ ไปยังอีกรายการหนึ่งและเป็นเพียงการตีความไบต์ที่แสดงถึงโครงสร้างเหล่านั้นอีกครั้ง Java ไม่ทำงานในลักษณะนั้น

generics ใน Java และ C ++ นั้นแตกต่างกันอย่างมาก อย่ากังวลกับตัวเองมากเกินไปว่าคุณทำสิ่ง C ++ ใน Java อย่างไร คุณต้องเรียนรู้วิธีการทำสิ่งต่างๆด้วยวิธี Java


ข้อมูลทั้งหมดไม่ได้ถูกเก็บไว้ในรันไทม์ ดังที่คุณเห็นจากตัวอย่างของฉัน(Bar) fooสร้างข้อผิดพลาดในเวลาคอมไพล์ แต่Bar.class.cast(foo)ไม่ ในความคิดของฉันถ้ามันถูกใช้ในลักษณะนี้ควร
Alexander Pogrebnyak

5
@ Alexander Pogrebnyak: อย่าทำอย่างนั้น! Bar.class.cast(foo)บอกคอมไพเลอร์อย่างชัดเจนว่าคุณต้องการทำการแคสต์ในรันไทม์ หากคุณต้องการตรวจสอบเวลาคอมไพล์เกี่ยวกับความถูกต้องของนักแสดงทางเลือกเดียวของคุณคือทำการแคสต์(Bar) fooสไตล์
Daniel Pryden

คุณคิดว่าวิธีการทำเหมือนกันคืออะไร? เนื่องจาก java ไม่รองรับการสืบทอดหลายคลาส
Yamur

13

Class.cast()ไม่ค่อยเคยใช้ในโค้ด Java หากมีการใช้งานมักจะเป็นประเภทที่รู้จักเฉพาะในรันไทม์เท่านั้น (เช่นผ่านClassอ็อบเจ็กต์ที่เกี่ยวข้องและพารามิเตอร์บางประเภท) มีประโยชน์มากในโค้ดที่ใช้ generics เท่านั้น (นั่นคือเหตุผลที่ไม่ได้เปิดตัวก่อนหน้านี้)

มันไม่คล้ายกับreinterpret_castเพราะมันจะไม่อนุญาตให้คุณทำลายระบบประเภทในขณะรันไทม์ได้มากกว่าที่แคสต์ปกติทำ (เช่นคุณสามารถทำลายพารามิเตอร์ประเภททั่วไปได้ แต่ไม่สามารถแบ่งประเภท "จริง" ได้)

ความชั่วร้ายของตัวดำเนินการแคสต์สไตล์ C โดยทั่วไปไม่ได้ใช้กับ Java โค้ด Java ที่ดูเหมือนแคสต์สไตล์ C นั้นคล้ายคลึงdynamic_cast<>()กับชนิดอ้างอิงใน Java มากที่สุด (โปรดจำไว้ว่า: Java มีข้อมูลประเภทรันไทม์)

โดยทั่วไปการเปรียบเทียบตัวดำเนินการการหล่อ C ++ กับการหล่อ Java นั้นค่อนข้างยากเนื่องจากใน Java คุณสามารถส่งการอ้างอิงได้เท่านั้นและจะไม่มีการแปลงเกิดขึ้นกับวัตถุ (เฉพาะค่าดั้งเดิมเท่านั้นที่สามารถแปลงได้โดยใช้ไวยากรณ์นี้)


dynamic_cast<>()ด้วยประเภทการอ้างอิง
Tom Hawtin - แท

@ ทอม: การแก้ไขนั้นถูกต้องหรือไม่? C ++ ของฉันเป็นสนิมมากฉันต้อง re-google เป็นจำนวนมาก ;-)
Joachim Sauer

+1 สำหรับ: "ความชั่วร้ายของตัวดำเนินการแคสต์สไตล์ C โดยทั่วไปใช้ไม่ได้กับ Java" ค่อนข้างจริง. ฉันกำลังจะโพสต์คำพูดที่แน่นอนเหล่านั้นเป็นความคิดเห็นในคำถาม
Daniel Pryden

4

โดยทั่วไปแล้วผู้ดำเนินการแคสต์จะนิยมใช้วิธีการแคสต์ Class # เนื่องจากมีความกระชับมากขึ้นและคอมไพเลอร์สามารถวิเคราะห์ได้เพื่อคายปัญหาที่ชัดเจนเกี่ยวกับโค้ด

Class # cast รับผิดชอบในการตรวจสอบประเภทในขณะทำงานแทนที่จะเป็นระหว่างการคอมไพล์

มีกรณีการใช้งานสำหรับนักแสดงระดับ # อย่างแน่นอนโดยเฉพาะอย่างยิ่งเมื่อต้องใช้การสะท้อนแสง

ตั้งแต่ lambda มาถึง java โดยส่วนตัวแล้วฉันชอบใช้ Class # cast กับคอลเลกชัน / สตรีม API ตัวอย่างเช่นถ้าฉันทำงานกับประเภทนามธรรม

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 ++ และ Java เป็นภาษาที่แตกต่างกัน

ตัวดำเนินการ Cast Java C-style ถูก จำกัด มากกว่าเวอร์ชัน C / C ++ การแคสต์ Java อย่างมีประสิทธิภาพก็เหมือนกับ C ++ dynamic_cast หากอ็อบเจ็กต์ที่คุณมีไม่สามารถแคสต์ไปยังคลาสใหม่ได้คุณจะได้รับเวลารัน (หรือหากมีข้อมูลเพียงพอในโค้ดเป็นเวลาคอมไพล์) ดังนั้นแนวคิด C ++ ในการไม่ใช้แคสต์ประเภท C จึงไม่ใช่ความคิดที่ดีใน Java


0

นอกเหนือจากการลบคำเตือนการร่ายที่น่าเกลียดตามที่กล่าวไว้มากที่สุด Class.cast ยังใช้การร่ายแบบรันไทม์ซึ่งส่วนใหญ่จะใช้กับการแคสต์ทั่วไปเนื่องจากข้อมูลทั่วไปจะถูกลบออกในขณะดำเนินการและบางส่วนจะพิจารณาว่าวัตถุทั่วไปแต่ละรายการจะถูกพิจารณาอย่างไรซึ่งจะนำไปสู่การไม่ โยน ClassCastException ในช่วงต้น

ตัวอย่างเช่น serviceLoder ใช้เคล็ดลับนี้เมื่อสร้างวัตถุตรวจสอบ S p = service.cast (c.newInstance ()); สิ่งนี้จะทำให้เกิดข้อยกเว้นการร่ายคลาสเมื่อ SP = (S) c.newInstance (); จะไม่และอาจแสดงคำเตือน'Type safety: Unchecked cast from Object to S' (เช่นเดียวกับ Object P = (Object) c.newInstance ();)

- เพียงแค่ตรวจสอบว่าวัตถุที่ถูกหล่อนั้นเป็นตัวอย่างของคลาสการหล่อจากนั้นมันจะใช้ตัวดำเนินการโยนเพื่อส่งและซ่อนคำเตือนโดยการระงับมัน

การใช้งาน java สำหรับการส่งแบบไดนามิก:

@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

โดยส่วนตัวแล้วฉันเคยใช้สิ่งนี้มาก่อนเพื่อสร้างตัวแปลง JSON เป็น POJO ในกรณีที่ JSONObject ประมวลผลด้วยฟังก์ชันมีอาร์เรย์หรือ JSONObjects ที่ซ้อนกัน (หมายความว่าข้อมูลที่นี่ไม่ใช่ประเภทดั้งเดิมหรือString) ฉันพยายามเรียกใช้เมธอด setter โดยใช้class.cast()ในลักษณะนี้:

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))));
    }
    ...
}

ไม่แน่ใจว่าสิ่งนี้มีประโยชน์อย่างมากหรือไม่ แต่อย่างที่กล่าวไว้ก่อนหน้านี้การสะท้อนกลับเป็นหนึ่งในกรณีการใช้งานที่ถูกต้องตามกฎหมายเพียงไม่กี่กรณีที่class.cast()ฉันคิดได้อย่างน้อยคุณก็มีอีกตัวอย่างหนึ่งในตอนนี้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.