• Java入门记(二):向上转型与向下转型


      在对Java学习的过程中,对于转型这种操作比较迷茫,特总结出了此文。例子参考了《Java编程思想》。

        目录

    几个同义词

    向上转型与向下转型

      例一:向上转型,调用指定的父类方法

      例二:向上转型,动态绑定

      例三:向上转型,静态绑定

      例四:向下转型

    转型的误区

      1.运行信息(RTTI)

      2.数组类型

      3.Java容器

    几个同义词

      首先是几组同义词。它们出现在不同的书籍上,这是造成理解混淆的原因之一。

      父类/超类/基类

      子类/导出类/继承类/派生类

      静态绑定/前期绑定

      动态绑定/后期绑定/运行时绑定

    转型与向转型

    例一:向上转型,调用指定的父类方法

    class Shape {
      
    static void draw(Shape s) { System.out.println("Shape draw."); } } class Circle extends Shape {
      static void draw(Circle c) { System.out.println("Circle draw."); } } public class CastTest { public static void main(String args[]) { Circle c = new Circle(); Shape.draw(c); } }

    输出为

    Shape draw.

      这表明,draw(Shape s)方法本来被设计为接受Shape引用,但这里传递的是Circle引用。实际上draw(Shape s)方法可以对所有Shape类的导出类使用,这被称为向上转型。表现的行为,和方法所属的类别一致。换句话说,由于明确指出是父类Shape的方法,那么其行为必然是这个方法对应的行为,没有任何歧义可言。

      “向上转型”的命名来自于类继承图的画法:根置于顶端,然后逐渐向下,以本例中两个类为例,如下图所示:

    例二:向上转型,动态绑定

    class Shape {
        public void draw() {
            System.out.println("Shape draw.");
        }
    }
    
    class Circle extends Shape {
        public void draw() {
            System.out.println("Circle draw.");
        }
    }
    
    public class CastTest {
        public static void drawInTest(Shape s) {
            s.draw();
        }
        public static void main(String args[]) {
            Circle c = new Circle();
            drawInTest(c);
        }
    }

    输出为

      Circle draw.

      这样做的原因是,一个drawInTest(Shape s)就可以处理Shape所有子类,而不必为每个子类提供自己的方法。但这个方法能能调用父类和子类所共有的方法,即使二者行为不一致,也只会表现出对应的子类方法的行为。这是多态所允许的,但容易产生迷惑。

    例三:向上转型,静态绑定

    class Shape {
        public static void draw() {
            System.out.println("Shape draw.");
        }
    }
    
    class Circle extends Shape {
        public static void draw() {
            System.out.println("Circle draw.");
        }
    }
    
    public class CastTest {
        public static void drawInTest(Shape s) {
            s.draw();
        }
        public static void main(String args[]) {
            Circle c = new Circle();
            drawInTest(c);
        }
    }

    输出为

      Shape draw.

      例三与例二有什么区别?细看之下才会发现,例三里调用的方法被static修饰了,得到了完全不同的结果。

      这两例行为差别的原因是:Java中除了static方法和final方法(包括private方法),其他方法都是动态绑定的。对于一个传入的基类引用,后期绑定能够正确的识别其所属的导出类。加了static,自然得不到这个效果了。

      了解了这一点之后,就可以明白为什么要把例一写出来了。例一中的代码明确指出调用父类方法,而例三调用哪个方法是静态绑定的,不是直接指明的,稍微绕了一下。

    例四:向下转型

      出自《Java编程思想》8.5.2节,稍作了修改,展示如何通过类型转换获得子类独有方法的访问方式。

      这相当于告诉了编译器额外的信息,编译器将据此作出检查。

    class Useful {
        public void f() {System.out.println("f() in Useful");}
        public void g() {System.out.println("g() in Useful");}
    }
    
    class MoreUseful extends Useful {
        public void f() {System.out.println("f() in MoreUseful");}
        public void g() {System.out.println("g() in MoreUseful");}
        public void u() {System.out.println("u() in MoreUseful");}
    
    }
    
    public class RTTI {
        public static void main(String[] args) {
            Useful[] x = {
                new Useful(),
                new MoreUseful()
            };
            x[0].f();
            x[1].g();
            // Compile-time: method not found in Useful:
            //! x[1].u();
            ((MoreUseful)x[1]).u(); // Downcast/RTTI
            ((MoreUseful)x[0]).u(); // Exception thrown
        }
    }    

    输出

    Exception in thread "main" java.lang.ClassCastException: Useful cannot be cast to MoreUseful
    at RTTI.main(RTTI.java:44)
    f() in Useful
    g() in MoreUseful
    u() in MoreUseful

      虽然父类Useful类型的x[1]接收了一个子类MoreUseful对象的引用,但仍然不能直接调用其子类中的u()方法。如果需要调用,需要做向下转型。这种用法很常见,比如一个通用的方法,处理的入参是一个父类,处理时根据入参的类型信息转化成对应的子类使用不同的逻辑处理。 

      此外,父类对象不能向下转换成子类对象

      向下转型的好处,在学习接口时会明显地体会出来(如果把实现接口看作多重继承)。可以参考9.4节的例子,这里不做详述:

    interface CanFight {
        void fight();
    }
    interface CanSwim {
        void swim();
    }
    interface CanFly {
        void fly();
    }
    
    class ActionCharacter {
        public void fight() {}
    }
    
    class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
        public void swim() {}
        public void fly() {}
    }
    
    public class Adventure {
        static void t(CanFight x) { x.fight(); }
        static void u(CanSwim x) { x.swim(); }
        static void v(CanFly x) { x.fly(); }
        static void w(ActionCharacter x) { x.fight(); }
        public static void main(String[] args) {
            Hero i = new Hero();
            t(i); // Treat it as a CanFight
            u(i); // Treat it as a CanSwim
            v(i); // Treat it as a CanFly
            w(i); // Treat it as an ActionCharacter
        }
    }

    转型的误区

      转型很方便,利用转型可以写出灵活的代码。不过,如果用得随心所欲而忘乎所以的话,难免要跌跟头。下面是几种看似可以转型,实际会导致错误的情形。

    1.运行信息(RTTI)

    /* 本例代码节选自《Java编程思想》14.2.2节 */
    
    Class<Number> genericNumberClass = int.class

      这段代码是无效的,编译不能通过,即使把int换为Integer也同样不通过。虽然int的包装类Integer是Number的子类,但Integer Class对象并不是Number Class对象的子类。

    2.数组类型

    /* 代码节改写《Java编程思想》15.8.2节,本例与泛型与否无关。 */
    
    class Generic<T> {}
    
    public class ArrayOfGeneric {
        static final int SIZE = 100;
        static Generic<Integer>[] gia;
        @SuppressWarnings("unchecked")
        public static void main(String[] args) {
            //! gia = (Generic<Integer>[]) new Object[SIZE];
            gia = (Generic<Integer>[]) new Generic[SIZE];
        }
    }

      注释部分在去掉注释后运行会提示java.lang.ClassCastException。这里令人迷惑的地方在于,子类数组类型不是父类数组类型的子类。在异常提示的后面可以看到

    [Ljava.lang.Object; cannot be cast to [LGeneric;

      除了通过控制台输出的异常信息,可以使用下面的代码来看看gia究竟是什么类型:

            Object[] obj = new Object[SIZE];
            gia = (Generic<Integer>[]) new Generic[SIZE];
            System.out.println(obj.getClass().getName());
            System.out.println(gia.getClass().getName());
            System.out.println(obj.getClass().getClass().getName());
            System.out.println(gia.getClass().getSuperclass().getName());

    控制台输出为:

    [Ljava.lang.Object;
    [LGeneric;
    java.lang.Object
    java.lang.Object

      可见,由Generic<Integer>[] gia和Object[] obj定义出的gia和obj根本没有任何继承关系,自然不能类型转换,不管这个数组里是否放的是子类的对象。(子类对象是可以通过向上转型获得的,如果被转换的确实是一个子类对象,见例四)

    3.Java容器

    /* 代码节选自《Java编程思想》15.10节*/
    class Fruit {}
    class Apple extends Fruit {}
    class Orange extends Fruit {}
    public class Test {
        public static void main(String[] args) {
        // 无法编译
        List<Fruit> fruitList = new ArrayList<Apple>();
        }
    }

      明明Fruit的List是可以存放Apple对象的,为什么赋值失败?其实这根本不是向上转型。虽然可以通过getClass().getName()得知List<Fruit>和List<Apple>同属java.util.ArrayList类型,但是,假设这里可以编译通过,相当于允许向ArrayList<Apple>存放一个Orange对象,显然是不合理的。虽然由于泛型的擦除,ArrayList<Fruit>和ArrayList<Apple>在运行期是同一种类型,但是具体能持有的元素类型会在编译期进行检查。

  • 相关阅读:
    前端和后端的区别和分工
    IntelliJ和tomcat中的目录结构
    JAVA开发环境和IntelliJ工具安装
    Linux下Python+Selenium自动化模拟用户登录(备注:记录一次强行卸载rpm依赖包,引发的rpm、yum等命令异常,无法远程xftp工具)
    Docker 操作命令梳理(镜像下载、容器创建、Dockerfile)
    Centos 6.6 Docker安装(内网坏境)
    C# 连接Access2010 数据库之初探
    C#使用NLog记录日志
    现实两种
    C#中的两把双刃剑:抽象类和接口
  • 原文地址:https://www.cnblogs.com/wuyuegb2312/p/java-cast.html
Copyright © 2020-2023  润新知