美文网首页
Java中的类型转换-高级进阶

Java中的类型转换-高级进阶

作者: 油腻的Java | 来源:发表于2019-05-29 18:46 被阅读0次
    java-scripts.jpg

    概述

    我们知道Java类型系统由两种类型组成:基础类型和封装类型。

    向上转型

    从子类到超类的转换称为向上转型。通常,向上是由编译器隐式执行的。

    向上转型与继承密切相关 - 这是Java中的另一个核心概念。使用引用变量来引用更具体的类型是很常见的。每次我们这样做时,都会发生隐式的向上转型。

    我们定义一个Animal类:

    public class Animal {
     
        public void eat() {
            // ... 
        }
    }
    

    现在我们来扩展Animal:

    public class Cat extends Animal {
     
        public void eat() {
             // ... 
        }
     
        public void meow() {
             // ... 
        }
    }
    

    现在,我们可以创建一个对象Cat类,并把它分配给类型的引用变量cat:

    Cat cat = new Cat();

    我们还可以将它分配给Animal类型的引用变量:

    Animal animal = cat;

    在上面的分配中,发生了隐式的向上转换。我们可以明确地做到:

    animal = (Animal) cat;

    但是没有必要显式地继承继承树。编译器知道cat是Animal并且不显示任何错误。

    注意,该引用可以引用声明类型的任何子类型。

    使用向上转型,我们限制了Cat实例可用的方法数量,但没有更改实例本身。现在我们不能做任何特定于Cat的事情-我们不能在animal变量上调用meow()。

    虽然Cat对象仍然是Cat对象,但调用meow()会导致编译器错误:

    // animal.meow(); The method meow() is undefined for the type Animal

    要调用meow(),我们需要向下转型animal,我们稍后会这样做。

    但现在我们将描述是什么让我们向上转型,我们可以利用多态性。

    多态性

    让我们定义Animal的另一个子类,一个Dog类:

    public class Dog extends Animal {
     
        public void eat() {
             // ... 
        }
    }
    

    现在我们可以定义feed()方法来处理像动物一样的所有猫狗:

    public class AnimalFeeder {
     
        public void feed(List<Animal> animals) {
            animals.forEach(animal -> {
                animal.eat();
            });
        }
    }
    

    我们不希望AnimalFeeder关注列表中的哪种动物 - 猫或狗。在feed()方法中,它们都是动物。

    当我们将特定类型的对象添加到动物列表时,会发生隐式向上转型:

    List<Animal> animals = new ArrayList<>();
    animals.add(new Cat());
    animals.add(new Dog());
    new AnimalFeeder().feed(animals);
    

    我们添加了猫和狗,它们被隐含地转向了Animal类型。每只猫都是动物,每只狗都是动物。他们是多态的。

    顺便说一句,所有Java对象都是多态的,因为每个对象至少是一个Object。我们可以将一个Animal实例分配给Object类型的引用变量,编译器不会报错:

    Object object = new Animal();

    这就是为什么我们创建的所有Java对象都已经具有Object特定的方法,例如toString()。

    向上转型到接口也很常见。

    我们可以创建Mew接口并让Cat实现它:

    public interface Mew {
        public void meow();
    }
     
    public class Cat extends Animal implements Mew {
         
        public void eat() {
             // ... 
        }
     
        public void meow() {
             // ... 
        }
    }
    

    现在任何Cat对象也可以向上转换为Mew:

    Mew mew = new Cat();

    Cat是Mew,向上转型是合法的并且是隐含的。

    因此,Cat是Mew,Animal,Object和Cat。在我们的示例中,它可以分配给所有四种类型的引用变量。

    重写

    在上面的示例中,覆盖了eat()方法。这意味着尽管在Animal类型的变量上调用了eat(),但是工作是通过在真实对象上调用的方法完成的 - Cat和Dog:

    public void feed(List<Animal> animals) {
        animals.forEach(animal -> {
            animal.eat();
        });
    }
    

    如果我们在我们的类中添加一些日志记录,我们会看到Cat和Dog的方法被调用:

    2019-05-29 17:48:49,354 [main] INFO com.william.casting.Cat - cat is eating
    2019-05-29 17:48:49,363 [main] INFO com.william.casting.Dog - dog is eating
    

    总结一下:

    • 如果对象与变量的类型相同或者它是子类型,则引用变量可以引用对象
    • 向上发生隐含的上行
    • 所有Java对象都是多态的,并且由于向上转型可以被视为超类型的对象

    向下转型

    如果我们想使用Animal类型的变量来调用仅适用于Cat类的方法,该怎么办?这是一个向下转型。它是从超类到子类的转换。

    我们来举个例子:

    Animal animal = new Cat();
    

    我们知道动物变量是指Cat的实例。我们想在动物身上调用Cat的meow()方法。但编译器提示类型为Animal的meow()方法不存在。

    应该将Animal转向Cat:

    ((Cat) animal).meow();

    内括号和它们包含的类型有时称为强制转换运算符。请注意,编译代码也需要外部括号。

    让我们用meow()方法重写之前的AnimalFeeder示例:

    public class AnimalFeeder {
        public void feed(List<Animal> animals) {
            animals.forEach(animal -> {
                animal.eat();
                if (animal instanceof Cat) {
                    ((Cat) animal).meow();
                }
            });
        }
    }
    

    现在我们可以访问Cat类可用的所有方法。查看日志以确保实际调用了meow():

    2019-05-29 18:28:19,445 [main] INFO com.william.casting.Cat - cat is eating
    2019-05-29 18:28:19,454 [main] INFO com.william.casting.Cat - meow
    2019-05-29 18:28:19,455 [main] INFO com.william.casting.Dog - dog is eating
    

    请注意,在上面的示例中,我们尝试仅向下转换那些实际上是Cat实例的对象。为此,我们使用运算符instanceof。

    instanceof操作

    我们经常在向下转换之前使用instanceof运算符来检查对象是否属于特定类型:

    if (animal instanceof Cat) {
        ((Cat) animal).meow();
    }
    

    ClassCastException异常

    如果我们没有使用instanceof运算符检查类型,编译器就不会报错。但在运行时,会有一个异常。

    为了演示这个,让我们从上面的代码中删除instanceof运算符:

    public void uncheckedFeed(List<Animal> animals) {
        animals.forEach(animal -> {
            animal.eat();
            ((Cat) animal).meow();
        });
    }
    

    此代码编译没有问题。但如果我们尝试运行它,我们会看到一个异常:

    java.lang.ClassCastException:com.william.casting.Dog无法强制转换为com.william.casting.Cat
    

    这意味着我们正在尝试将作为Dog实例的对象转换为Cat实例。

    如果我们向下转型的类型与真实对象的类型不匹配,则ClassCastException总是在运行时抛出。

    注意,如果我们尝试向下转型为不相关的类型,编译器将不允许这样

    Animal animal;
    String s = (String) animal;
    

    编译器说“无法从Animal转换为String”。

    对于要编译的代码,两种类型都应该在同一继承树中。

    我们总结一下:

    • 为了获得特定于子类的成员的访问权,必须进行向下转换
    • 使用强制转换运算符完成向下转换
    • 要安全地向下转换对象,我们需要instanceof运算符
    • 如果真实对象与我们向下转换的类型不匹配,则将在运行时抛出ClassCastException

    Cast()方法

    还有另一种使用Class方法强制转换对象的方法:

    public void test() {
        Animal animal = new Cat();
        if (Cat.class.isInstance(animal)) {
            Cat cat = Cat.class.cast(animal);
            cat.meow();
        }
    }
    

    在上面的示例中,使用了cast()和isInstance()方法,而不是相应的cast和instanceof运算符。

    通常使用具有泛型类型的cast()和isInstance()方法。

    让我们用feed()方法创建AnimalFeederGeneric <T>类,它只“喂”一种类型的动物 - Cat或Dog,取决于类型参数的值:

    public class AnimalFeederGeneric<T> {
        private Class<T> type;
     
        public AnimalFeederGeneric(Class<T> type) {
            this.type = type;
        }
     
        public List<T> feed(List<Animal> animals) {
            List<T> list = new ArrayList<T>();
            animals.forEach(animal -> {
                if (type.isInstance(animal)) {
                    T objAsType = type.cast(animal);
                    list.add(objAsType);
                }
            });
            return list;
        }
     
    }
    

    的feed()方法检查每个Animal,并返回仅那些的实例Ť。

    注意,Class实例也应该传递给泛型类,因为我们无法从类型参数T中获取它。在我们的示例中,我们在构造函数中传递它。

    让我们使T等于Cat并确保该方法仅返回cat:

    @Test
    public void whenParameterCat_thenOnlyCatsFed() {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Cat());
        animals.add(new Dog());
        AnimalFeederGeneric<Cat> catFeeder
          = new AnimalFeederGeneric<Cat>(Cat.class);
        List<Cat> fedAnimals = catFeeder.feed(animals);
     
        assertTrue(fedAnimals.size() == 1);
        assertTrue(fedAnimals.get(0) instanceof Cat);
    }
    

    动态转换

    在Java 5之前,以下代码将是常态:

    List dates = new ArrayList();
    dates.add(new Date());
    Object object = dates.get(0);
    Date date = (Date) object;
    

    需要转换。虽然运行时类型是Date,但编译器无法知道它。

    使用泛型,可以重写上面的代码:

    List<Date> dates = new ArrayList<>();
    dates.add(new Date());
    Date date = dates.get(0);
    

    没有转换:由于泛型,编译器有足够的信息。

    强转换

    一个这样的用例是Servlet API。在servlet上下文/请求/会话中存储对象的映射不使用泛型。他们也会使用Object

    // In a servlet
    ServletContext context = getServletContext();
    context.put("date", new Date());
    
    // Somewhere else
    ServletContext context = getServletContext();
    Object object = context.get("date");
    Date date = (Date) object;
    

    ** 静态转换**

    使用Java进行强制转换的最常用方法如下:

    Object obj; // may be an integer
    if (obj instanceof Integer) {
        Integer objAsInt = (Integer) obj;
        // do something with 'objAsInt'
    }
    

    这使用了 instanceof和cast运算符。实例转换的类型(在本例中为 Integer)必须在编译时静态知道,所以让我们调用这个静态转换。

    如果 obj不是 Integer,则上述测试将失败。如果我们试图抛出它,我们会得到一个 ClassCastException。如果 obj为 null,则它会使instanceof测试失败 但可以被强制转换,因为 null可以是任何类型的引用。

    动态转换

    最初可用的唯一转换形式是静态转换。这意味着需要在编译时知道转换类型。但是,让我们设想一个接受a的方法Stream<Object>,过滤特定类型的所有元素,并以正确的类型返回这些元素。这是用法的一个例子:

    我遇到的一种技术不常使用Class上与运算符对应的方法 :

    Object obj; // may be an integer
    if (Integer.class.isInstance(obj)) {
        Integer objAsInt = Integer.class.cast(obj);
        // do something with 'objAsInt'
    }
    

    请注意,虽然在此示例中,要编译的类在编译时也是已知的,但不一定如此:

    Object obj; // may be an integer
    Class<T> type = // may be Integer.class
    if (type.isInstance(obj)) {
        T objAsType = type.cast(obj);
        // do something with 'objAsType'
    }
    

    因为类型在编译类型是未知的,我们将称之为动态转换。

    对于错误类型和空引用的实例,测试和强制转换的结果与静态强制转换的结果完全相同。

    现在

    转换Optional或Stream元素的值是一个两步过程:首先我们必须过滤掉错误类型的实例,然后我们可以转换为所需的类型。

    使用Class上的方法 ,我们使用方法引用来完成此操作。使用Optional的示例 :

    Optional<?> obj; // may contain an Integer
    Optional<Integer> objAsInt = obj
            .filter(Integer.class::isInstance)
            .map(Integer.class::cast);
    

    通过上面的写法,我们可以实现动态转换。

    再举一个案例

    List<?> items = ...
    List<Date> dates = filter(Date.class, items);
    

    改造

    static <T> List<T> filter(Class<T> clazz, List<?> items) {
        return items.stream()
            .filter(clazz::isInstance)
            .map(clazz::cast)
            .collect(Collectors.toList());
    }
    

    以上为动态转换的demo案例,用这个写法可以实现动态转换。

    总结

    本篇章介绍了Java类型转换的向上转换、向下转换、静态转换、动态转换。希望这些知识点可以对你有所帮助。

    相关文章

      网友评论

          本文标题:Java中的类型转换-高级进阶

          本文链接:https://www.haomeiwen.com/subject/lfqutctx.html