Java中显式类型转换的示例

34

我在http://www.javabeginner.com/learn-java/java-object-typecasting 上看到了这个例子,在它讲解显式类型转换的一部分中,有一个例子令我感到困惑。

例子如下:

class Vehicle {

    String name;
    Vehicle() {
        name = "Vehicle";
    }
}

class HeavyVehicle extends Vehicle {

    HeavyVehicle() {
        name = "HeavyVehicle";
    }
}

class Truck extends HeavyVehicle {

    Truck() {
        name = "Truck";
    }
}

class LightVehicle extends Vehicle {

    LightVehicle() {
        name = "LightVehicle";
    }
}

public class InstanceOfExample {

    static boolean result;
    static HeavyVehicle hV = new HeavyVehicle();
    static Truck T = new Truck();
    static HeavyVehicle hv2 = null;
    public static void main(String[] args) {
        result = hV instanceof HeavyVehicle;
        System.out.print("hV is an HeavyVehicle: " + result + "\n");
        result = T instanceof HeavyVehicle;
        System.out.print("T is an HeavyVehicle: " + result + "\n");
        result = hV instanceof Truck;
        System.out.print("hV is a Truck: " + result + "\n");
        result = hv2 instanceof HeavyVehicle;
        System.out.print("hv2 is an HeavyVehicle: " + result + "\n");
        hV = T; //Sucessful Cast form child to parent
        T = (Truck) hV; //Sucessful Explicit Cast form parent to child
    }
}

在T被分配引用hV并强制转换为(Truck)的最后一行中,为什么注释中说这是从父类向子类的成功显式转换?据我所知,强制转换(隐式或显式)只会更改对象的声明类型,而不是实际类型(除非您实际将新的类实例分配给该对象的字段引用)。如果hv已经分配了一个HeavyVehicle类的实例,该类是Truck类的超类,那么如何将此字段类型转换为更具体的子类Truck呢?
我理解的方式是,转换的目的是限制对对象(类实例)某些方法的访问。因此,您无法将对象转换为具有比对象实际分配的类更多方法的更具体的类。这意味着对象只能作为超类或与其实际实例化的类相同的类进行转换。这个理解正确吗?还是我的理解有误?我仍在学习,所以我不确定这是否是正确的看法。
我还理解到,这应该是下转换的示例,但是如果实际类型没有被向下转换的类的方法,我不确定这实际上是如何工作的。显式转换是否会以某种方式更改对象的实际类型(而不仅仅是声明类型),以便该对象不再是HeavyVehicle类的实例,而是Truck类的实例?

我们知道hV是卡车(或卡车的超类),因为上面一行的隐式转换起作用了(这也可以从声明的检查中明显看出)。您始终可以从对超类的引用显式地转换为对象的实际类型。 - Hot Licks
将一个类强制转换为其超类以有意限制可访问的方法是很少做的。事实上,我甚至不记得曾经这样做过。我想一些“最佳实践”专家可能会推荐这样做,但这些人很少与现实生活有任何联系。 - Hot Licks
1
hV = T; => 现在hV指向一辆卡车。 - njzk2
6个回答

67

引用 vs 对象 vs 类型

对我而言,关键在于理解对象和它的引用之间的区别,或者换句话说,对象和它的类型之间的区别。

当我们在Java中创建一个对象时,我们声明了它的真实本质,这将永远不会改变(例如new Truck())。但是,在Java中任何给定的对象都可能具有多种类型。其中一些类型显然由类层次结构给出,其他一些类型则不太明显(即泛型、数组)。

特别是对于引用类型,类层次结构决定了子类型规则。例如,在您的例子中,所有卡车都是重型车所有重型车都是车辆。因此,这种is-a关系的层次结构决定了卡车具有多个兼容类型

当我们创建一个Truck时,我们定义了一个“引用”来访问它。这个引用必须有其中一个兼容类型。

Truck t = new Truck(); //or
HeavyVehicle hv = new Truck(); //or
Vehicle h = new Truck() //or
Object o = new Truck();

重点在于意识到“对对象的引用并不是对象本身”。创建的对象的本质永远不会改变,但是我们可以使用不同类型的兼容引用来访问该对象。这是多态的一个特性。同一对象可以通过不同“兼容”的引用进行访问。
当我们进行任何类型的强制类型转换时,实际上是假定了不同类型引用之间的兼容性。
向上转型或扩展引用转换
现在,假设我们有一个类型为“Truck”的引用,我们可以轻松地推断出它始终与类型为“Vehicle”的引用兼容,因为所有卡车都是车辆。因此,我们可以进行向上转型引用,而无需使用显式转换。
Truck t = new Truck();
Vehicle v = t;

这也被称为扩展引用转换,因为随着类型层次结构的上升,类型变得更加通用。如果您愿意,可以在此处使用显式转换,但这是不必要的。我们可以看到,由t和v引用的实际对象是相同的。它始终是一个卡车。 向下转型或缩小引用转换 现在,拥有类型为Vechicle的引用,我们无法“安全地”得出它实际上引用了一辆卡车。毕竟,它也可能引用其他形式的交通工具。例如
Vehicle v = new Sedan(); //a light vehicle

如果在您的代码中发现 v 引用,但不知道它引用到哪个具体对象,则无法“安全地”断言它是指向一辆卡车还是轿车或其他类型的车辆。

编译器知道它无法保证被引用的对象的真实性质。但是程序员可以通过阅读代码来确定自己在做什么。就像上面的例子中,您可以清晰地看到 Vehicle v 是引用一个 Sedan
在这些情况下,我们可以进行向下转型。我们之所以这样称呼它,是因为我们正在沿着类型层次结构向下走。我们也称之为缩小引用转换。我们可以说
Sedan s = (Sedan) v;

这总是需要明确的强制转换,因为编译器无法确定这是否安全,这就像询问程序员“你确定自己在做什么吗?”。如果你欺骗了编译器,当执行代码时会抛出ClassCastException其他子类型规则 Java中还有其他子类型规则。例如,还有一个称为数字提升的概念,它会自动将表达式中的数字强制转换。比如:
double d = 5 + 6.0;

在此情况下,由整数和双精度浮点数组成的表达式在计算前将整数强制转换为双精度浮点数,从而得到一个双精度浮点数值。
你也可以进行原始类型的向上转型和向下转型。例如:
int a = 10;
double b = a; //upcasting
int c = (int) b; //downcasting

在这些情况下,如果可能会丢失信息,则需要进行显式转换。
有些子类型规则可能并不那么明显,比如数组的情况。例如,所有引用数组都是 Object[] 的子类型,但原始数组则不是。
在泛型的情况下,特别是使用像superextends这样的通配符时,情况变得更加复杂。就像在……中一样。
List<Integer> a = new ArrayList<>();
List<? extends Number> b = a;
        
List<Object> c = new ArrayList<>(); 
List<? super Number> d = c;

a的类型是b类型的子类型。而c的类型是d类型的子类型。

使用协变,无论何时出现List<? extends Number>,都可以传递List<Integer>,因此List<Integer>List<? extends Number>的子类型。

逆变产生类似的效果,无论何时出现类型List<? super Number>,都可以传递List<Object>,这使得List<Object>成为List<? super Number>的子类型。

还有装箱和拆箱受到一些强制转换规则的限制(我认为这也是某种形式的强制转换)。


8
这真的帮助我理解了什么时候应该使用显式转换。 - Edward J Beckett
对象的引用实际上包含了堆上对象的地址。如果你这样写:Truck t = new Truck(); System.out.print(t); 它会打印出类似于@6bc7c054的东西,这就是t在堆上的位置。 - Niclas Lindgren
1
引用类型与对象/运行时类型的非常有帮助的解释。 - K. hervey

5
你说得对。只有将对象强制转换到其类、一些父类或其父类实现的某些接口才能成功进行类型转换。如果你将它强制转换为一些父类或接口,则可以将其强制转换回原来的类型。
否则(虽然源代码中可能存在),它将导致运行时ClassCastException。
通常使用强制转换将不同的东西(例如,所有车辆都是相同接口或父类)存储在同一个字段或相同类型的集合中(例如,车辆),以便以同样的方式处理它们。
如果您想要完全访问,那么您可以将它们强制转换回去(例如,从Vehicle转换为Truck)。
在这个例子中,我非常确定最后一句话是无效的,注释只是错误的。

2
当您将Truck对象转换为HeavyVehicle时,可以这样进行强制类型转换:
Truck truck = new Truck()
HeavyVehicle hv = truck;

这个对象仍然是一辆卡车,但是你只能使用HeavyVehicle引用访问heavyVehicle的方法和字段。如果你再次将其向下转换为卡车,你就可以再次使用所有卡车的方法和字段。

Truck truck = new Truck()
HeavyVehicle hv = truck;
Truck anotherTruckReference = (Truck) hv; // Explicit Cast is needed here

如果您试图将一个非卡车对象向下转型,就会像以下示例一样抛出ClassCastException:
HeavyVehicle hv = new HeavyVehicle();
Truck tr = (Truck) hv;  // This code compiles but will throw a ClasscastException

出现异常是因为实际对象不是正确类的对象,而是超类(HeavyVehicle)的对象。


2
代码的最后一行成功编译并运行,没有出现任何异常。它所做的是完全合法的。
  1. hV initially refers to an object of type HeavyVehicle (let's call this object h1):

    static HeavyVehicle hV = new HeavyVehicle(); // hV now refers to h1.
    
  2. Later, we make hV refer to a different object, of type Truck (let's call this object t1):

    hV = T; // hV now refers to t1.
    
  3. Lastly, we make T refer to t1.

    T = (Truck) hV; // T now refers to t1.
    
T已经指向了t1,所以这个语句并没有改变任何东西。
如果hv已经被分配为HeavyVehicle类的一个实例,该类是Truck类的超类,那么如何将此字段强制转换为更具体的子类Truck,它是从HeavyVehicle类派生的?
在我们到达最后一行时,hV不再引用HeavyVehicle的实例。它引用Truck的一个实例。将Truck的实例类型转换为类型Truck没有问题。
这意味着对象只能作为其实际创建的类的超类或相同类进行转换。这样说没错吗?还是我理解错了?
基本上是对的,但不要将对象本身与引用对象的变量混淆。请参见下文。
显式转换是否会以某种方式改变对象的实际类型(不仅是声明的类型),使得对象不再是HeavyVehicle类的实例,而现在成为Truck类的实例?
不是的。一旦创建了对象,就无法更改其类型。它不能成为另一个类的实例。
重申一下,最后一行没有改变任何东西。T在那行之前指向t1,在那行之后仍指向t1。
那么为什么最后一行需要显式转换(Truck)?我们只是帮助编译器。
我们知道在那时,hV引用的是Truck类型的对象,因此将该类型的Truck对象分配给变量T是可以的。但编译器不聪明到知道这一点。编译器希望我们保证,在到达那行并尝试进行赋值时,它会找到一个Truck实例等待它。

1
为了更好地阐明上述观点,我修改了问题中的代码,并添加了更多的代码以及内联注释(包括实际输出),具体如下:
class Vehicle {

        String name;
        Vehicle() {
                name = "Vehicle";
        }
}

class HeavyVehicle extends Vehicle {

        HeavyVehicle() {
                name = "HeavyVehicle";
        }
}

class Truck extends HeavyVehicle {

        Truck() {
                name = "Truck";
        }
}

class LightVehicle extends Vehicle {

        LightVehicle() {
                name = "LightVehicle";
        }
}

public class InstanceOfExample {

        static boolean result;
        static HeavyVehicle hV = new HeavyVehicle();
        static Truck T = new Truck();
        static HeavyVehicle hv2 = null;
        public static void main(String[] args) {

                result = hV instanceof HeavyVehicle;
                System.out.print("hV is a HeavyVehicle: " + result + "\n"); // true

                result = T instanceof HeavyVehicle;
                System.out.print("T is a HeavyVehicle: " + result + "\n"); // true
//      But the following is in error.              
//      T = hV; // error - HeavyVehicle cannot be converted to Truck because all hV's are not trucks.                               

                result = hV instanceof Truck;
                System.out.print("hV is a Truck: " + result + "\n"); // false               

                hV = T; // Sucessful Cast form child to parent.
                result = hV instanceof Truck; // This only means that hV now points to a Truck object.                            
                System.out.print("hV is a Truck: " + result + "\n");    // true         

                T = (Truck) hV; // Sucessful Explicit Cast form parent to child. Now T points to both HeavyVehicle and Truck. 
                                // And also hV points to both Truck and HeavyVehicle. Check the following codes and results.
                result = hV instanceof Truck;                             
                System.out.print("hV is a Truck: " + result + "\n");    // true 

                result = hV instanceof HeavyVehicle;
                System.out.print("hV is a HeavyVehicle: " + result + "\n"); // true             

                result = hV instanceof HeavyVehicle;
                System.out.print("hV is a HeavyVehicle: " + result + "\n"); // true 

                result = hv2 instanceof HeavyVehicle;               
                System.out.print("hv2 is a HeavyVehicle: " + result + "\n"); // false

        }

}

1
以上代码将编译并运行良好。现在更改上面的代码,添加以下行System.out.println(T.name);
这将确保您在将hV对象向下转换为Truck后不再使用对象T。
目前,在您的代码中,您在向下转换后没有使用T,所以一切正常工作。
这是因为,通过显式将hV强制转换为Truck,编译器不会抱怨,认为程序员已经将对象转换到了什么地方。
但是在运行时,JVM无法证明转换,并引发ClassCastException“ HeavyVehicle cannot be cast to Truck”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接