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

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类型转换的向上转换、向下转换、静态转换、动态转换。希望这些知识点可以对你有所帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350