方法继承,“超级”保留字和Java继承中的ClassCastExceptions
继承和组合是开发人员用来在类和对象之间建立关系的两种编程技术。继承是从另一类继承一个类,而组合将一个类定义为其部分的总和。
通过继承创建的类和对象紧密耦合,因为在继承关系中更改父类或超类可能会破坏您的代码。通过合成创建的类和对象是松散耦合的,这意味着您可以更轻松地更改组件,而不会破坏代码。
因为松散耦合的代码提供了更大的灵活性,所以许多开发人员已经了解到,组合是比继承更好的技术,但事实更复杂。选择编程工具类似于选择正确的厨房工具:您不会使用黄油刀切菜,并且同样的,您不应为每种编程场景都选择成分。
[ 在这个由12部分组成的综合课程中,从入门概念到高级设计模式学习Java!]
在此Java Challenger中,您将学习继承和组合之间的区别,以及如何确定哪个对您的程序正确。接下来,我将向您介绍Java继承的几个重要但具有挑战性的方面:方法覆盖,super关键字和类型转换。最后,您将通过逐行处理一个继承示例来确定所输出的内容,以测试所学内容。
何时在Java中使用继承
在面向对象的编程中,当我们知道孩子与其父类之间存在“是”关系时,就可以使用继承。一些示例是:
A person is a human.A cat is an animal.A car is a vehicle.
在每种情况下,子类或子类都是父类或超类的专门版本。从超类继承是代码重用的一个示例。为了更好地理解这种关系,请花点时间学习Car该类,该类继承自Vehicle:
class Vehicle { String brand; String color; double weight; double speed; void move() { System.out.println("The vehicle is moving"); }}public class Car extends Vehicle { String licensePlateNumber; String owner; String bodyStyle; public static void main(String... inheritanceExample) { System.out.println(new Vehicle().brand); System.out.println(new Car().brand); new Car().move(); }}
当您考虑使用继承时,请问自己子类是否真的是超类的更专门的版本。在这种情况下,汽车是车辆的一种,因此继承关系很有意义。
何时在Java中使用合成
在面向对象的编程中,我们可以在一个对象“具有”(或属于)另一个对象的情况下使用组合。一些示例是:
A car has a battery (a battery is part of a car).A person has a heart (a heart is part of a person).A house has a living room (a living room is part of a house).
为了更好地理解这种类型的关系,请考虑a的组成House:
public class CompositionExample { public static void main(String... houseComposition) { new House(new Bedroom(), new LivingRoom()); // The house now is composed with a Bedroom and a LivingRoom } static class House { Bedroom bedroom; LivingRoom livingRoom; House(Bedroom bedroom, LivingRoom livingRoom) { this.bedroom = bedroom; this.livingRoom = livingRoom; } } static class Bedroom { } static class LivingRoom { }}
在这种情况下,我们知道一所房子有一个客厅和一间卧室,因此我们可以使用Bedroom和 LivingRoom对象构成一个House。
继承与组成:两个例子
考虑下面的代码。这是继承的好例子吗?
import java.util.HashSet;public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); }
在这种情况下,答案是否定的。子类继承了许多永远不会使用的方法,导致紧密耦合的代码既混乱又难以维护。如果仔细观察,很显然此代码未通过“是”测试。
现在,让我们尝试使用组合的相同示例:
import java.util.HashSet;import java.util.Set;public class CharacterCompositionExample { static Set set = new HashSet<>(); public static void main(String... goodExampleOfComposition) { set.add("Homer"); set.forEach(System.out::println); }
在这种情况下使用composition允许 CharacterCompositionExample类仅使用的两个HashSet方法,而无需继承所有方法。这样可以简化代码,减少耦合,使代码更易于理解和维护。
JDK中的继承示例
Java Development Kit充满了很好的继承示例:
class IndexOutOfBoundsException extends RuntimeException {...}class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...}class FileWriter extends OutputStreamWriter {...}class OutputStreamWriter extends Writer {...}interface Stream extends BaseStream> {...}
注意,在每个示例中,子类都是其父类的专门版本;例如,IndexOutOfBoundsException是的一种RuntimeException。
用Java继承重写方法
继承使我们可以在新类中重用一个类的方法和其他属性,这非常方便。但是,要使继承真正起作用,我们还需要能够在新的子类中更改某些继承的行为。例如,我们可能想专门制造一种声音Cat:
class Animal { void emitSound() { System.out.println("The animal emitted a sound"); }}class Cat extends Animal { @Override void emitSound() { System.out.println("Meow"); }}class Dog extends Animal {}public class Main { public static void main(String... doYourBest) { Animal cat = new Cat(); // Meow Animal dog = new Dog(); // The animal emitted a sound Animal animal = new Animal(); // The animal emitted a sound cat.emitSound(); dog.emitSound(); animal.emitSound(); }}
这是带有方法覆盖的Java继承的示例。首先,我们扩展的Animal类来创建一个新的Cat类。接下来,我们覆盖的Animal类的emitSound()方法来获取特定声音的Cat品牌。即使我们将类类型声明为Animal,当我们实例化它时Cat也会得到猫的叫声。
方法重载是多态
您可能还记得我上一篇文章中的方法重写是多态或虚拟方法调用的示例。
Java是否具有多重继承?
与某些语言(例如C ++)不同,Java不允许对类进行多重继承。但是,您可以对接口使用多重继承。在这种情况下,类和接口之间的区别在于接口不保持状态。
如果您尝试像我在下面这样进行多重继承,则代码将无法编译:
class Animal {}class Mammal {}class Dog extends Animal, Mammal {}
使用类的解决方案将是逐一继承:
class Animal {}class Mammal extends Animal {}class Dog extends Mammal {}
另一个解决方案是用接口替换类:
interface Animal {}interface Mammal {}class Dog implements Animal, Mammal {}
使用“超级”访问父类方法
当两个类通过继承相关联时,子类必须能够访问其父类的每个可访问字段,方法或构造函数。在Java中,我们使用保留字super来确保子类仍然可以访问其父类的重写方法:
public class SuperWordExample { class Character { Character() { System.out.println("A Character has been created"); } void move() { System.out.println("Character walking..."); } } class Moe extends Character { Moe() { super(); } void giveBeer() { super.move(); System.out.println("Give beer"); } }}
在此示例中,Character是Moe的父类。使用super,我们可以访问Character的 move()方法来给Moe啤酒。
将构造函数与继承一起使用
当一个类继承自另一个类时,在加载其子类之前,始终会先加载超类的构造函数。在大多数情况下,保留字super将自动添加到构造函数中。但是,如果超类在其构造函数中有一个参数,我们将不得不故意调用该super构造函数,如下所示:
public class ConstructorSuper { class Character { Character() { System.out.println("The super constructor was invoked"); } } class Barney extends Character { // No need to declare the constructor or to invoke the super constructor // The JVM will to that }}
如果父类的构造函数带有至少一个参数,则必须在子类中声明该构造函数,并使用super它显式调用父构造函数。该super保留字不会被自动添加,没有它的代码将无法编译。例如:
public class CustomizedConstructorSuper { class Character { Character(String name) { System.out.println(name + "was invoked"); } } class Barney extends Character { // We will have compilation error if we don't invoke the constructor explicitly // We need to add it Barney() { super("Barney Gumble"); } }}
类型转换和ClassCastException
强制转换是一种向编译器显式传达您确实打算转换给定类型的方法。就像说,“嘿,JVM,我知道我在做什么,所以请使用这种类型转换此类。” 如果您强制转换的类与声明的类类型不兼容,则将获得ClassCastException。
在继承中,我们可以在不强制转换的情况下将子类分配给父类,但是在不使用强制转换的情况下不能将父类分配给子类。
考虑以下示例:
public class CastingExample { public static void main(String... castingExample) { Animal animal = new Animal(); Dog dogAnimal = (Dog) animal; // We will get ClassCastException Dog dog = new Dog(); Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); Animal anotherDog = dog; // It's fine here, no need for casting System.out.println(((Dog)anotherDog)); // This is another way to cast the object }}class Animal { }class Dog extends Animal { void bark() { System.out.println("Au au"); } }
当我们尝试将Animal实例转换为时,Dog我们会得到一个异常。这是因为Animal对其子项一无所知。它可能是猫,鸟,蜥蜴等。没有关于特定动物的信息。
在这种情况下的问题是我们已经Animal像这样实例化了:
Animal animal = new Animal();
然后尝试将其投射如下:
Dog dogAnimal = (Dog) animal;
由于我们没有Dog实例,因此无法将分配Animal给Dog。如果尝试,将得到一个ClassCastException。
为了避免异常,我们应该Dog像这样实例化:
Dog dog = new Dog();
然后将其分配给Animal:
Animal anotherDog = dog;
在这种情况下,因为我们扩展了Animal类,所以Dog甚至不需要强制转换该实例。在Animal父类的类型只是接受任务。
用超类型进行转换
可以Dog使用超类型声明a Animal,但是如果我们想从中调用特定方法Dog,则需要对其进行强制转换。例如,如果我们想调用该bark()方法怎么办?该Animal超有没有办法知道到底是什么动物的情况下,我们是在调用,所以我们要投Dog之前手动我们可以调用bark()方法:
Animal dogWithAnimalType = new Dog();Dog specificDog = (Dog) dogWithAnimalType;specificDog.bark();
您也可以在不将对象分配给类类型的情况下使用强制转换。当您不想声明另一个变量时,此方法很方便:
System.out.println(((Dog)anotherDog)); // This is another way to cast the object
接受Java继承挑战!
您已经了解了一些重要的继承概念,因此现在是时候尝试继承挑战了。首先,请研究以下代码:
public class InheritanceCompositionChallenge { private static int result; public static void main(String... doYourBest) { Character homer = new Homer(); homer.drink(); new Character().drink(); ((Homer)homer).strangleBart(); Character character = new Character(); System.out.println(result); ((Homer)character).strangleBart(); } static class Character { Character() { result++; } void drink() { System.out.println("Drink"); } } static class Homer extends Character { Lung lung = new Lung(); void strangleBart() { System.out.println("Why you little!"); } void drink() { System.out.println("Drink beer"); lung.damageLungs(); } } static class Lung { void damageLungs() { System.out.println("Soon you will need a transplant"); } }}
运行main方法后输出的是什么?
A)
DrinkDrinkWhy you little!2Exception in thread "main" java.lang.ClassCastException:....
B)
Drink beerSoon you will need a transplantDrinkWhy you little!Exception in thread "main" java.lang.ClassCastException:....
C)
Drink beerSoon you will need a transplantDrinkWhy you little!3Exception in thread "main" java.lang.ClassCastException:....
D)
Drink beerSoon you will need a transplantDrinkWhy you little!2Why you little!
刚刚发生了什么?了解Java继承
继承质询的正确输出是C:
Drink beerSoon you will need a transplantDrinkWhy you little!3Exception in thread "main" java.lang.ClassCastException:....
要了解原因,只需从顶部开始检查代码即可:
Character homer = new Homer(); homer.drink();
由于我们使用Homer实例化了对象,因此将执行Homer方法实现,并产生以下输出:
Drink beerSoon you will need a transplant
然后,我们drink()直接从Character类中调用该方法。
new Character().drink();
我们将得到以下输出:
Drink beer
在这一行,我们使用强制转换并strangleBart()正常调用该方法:
((Homer)homer).strangleBart();
然后,我们要求输出结果:
System.out.println(result);
因为我们知道super构造函数总是必须被调用,所以我们只需要计算多少次Character或被Homer实例化。现在我们知道输出将是3。
最后,我们尝试从滥用的转换中调用方法:
((Homer) character).strangleBart();
我们尝试转换的类类型和实现Character,因此ClassCastException将抛出a。(这是因为该类中没有Homer信息Character。)
继承的常见错误
- 在仔细考虑之前编写继承代码。
- 当组合是更好的选择时,使用继承。
- 使用继承时不利用多态性。
- 测试“是”时使用继承不会通过。
关于Java继承的小知识
- 当用子类实例化超类时,将考虑子类实例化。
- 为了决定是否使用继承,请问自己子类是否确实是父类的特殊类型(“是”测试)。
- 该super保留字会自动在你的构造函数,如果你不自己声明它被添加。
- 如果具有父类类型和该类型的子类的实例化,则可以将其强制转换为子类。
- 如果超类的构造函数至少接收一个参数,则必须super在子类中调用此构造函数并传递所需的参数。
若有哪里不对,或您有其它看法,可以给我留言,若您想获取更多Java干货知识,可以关注我,每天Java干货更新不断~