组合与继承是人类在计算机世界中对客观事物的描述。
- 组合
- 继承
- 继承中的初始化流程
- 向上/向下转型
- final关键字
面向对象的三大特征:
- 封装
- 继承
- 多态
1. 继承和 Java
继承的本质是提炼出公用代码,避免重复。
Java 的继承体系是单根继承,所有类都继承自Object
类,重要的方法比如equals()
和toString()
。
C++ 这种多重继承带来的问题就是当两个父类有相同的东西时,子类无法做出选择。
例如,将属性name
和方法sayMyName
抽离到公共的Animal
类。
public class Animal {
String name;
public void sayMyName() {
System.out.println("我的名字是" + name);
}
}
public class Cat extends Animal {
public Cat(String name) {
this.name = name;
}
public void meow() {
System.out.println("喵" + name);
}
}
public class Dog extends Animal {
public Dog(String name) {
this.name = name;
}
public void wang() {
System.out.println("汪" + name);
}
}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat("ABC");
cat.meow();
cat.sayMyName();
Dog dog = new Dog("BCD");
dog.wang();
dog.sayMyName();
}
}
2. 类的结构与初始化顺序
- 子类拥有父类的一切数据和行为
- 父类先于子类进行初始化
- 必须拥有匹配的构造器
-
super
关键字
-
上面例子中,如果父类和子类都没有显式的声明构造器,那么编译器会自动给父类和子类添加默认的构造器:
public class Animal {
//...
public Animal() {
// 一个空的构造器
}
//...
}
public class Cat extends Animal {
public Cat() {
super(); // 超类的构造器
}
// ...
}
如代码中所示,super
关键字代表的就是父类的构造器,子类来源于父类,如此,当调用Cat
类的构造器创建Cat
实例时,内部先调用super
创建出父类的成员,然后再创建自身的成员。
还是上面的例子,如果父类一开始就自定义了构造器,那么子类中就要拥有匹配的构造器,编译器提供的默认构造器是没有参数的,这不匹配,所以需要子类的构造器中调用父类,并传入参数:
public class Animal {
String name;
// 父类自定义了构造器
public Animal(String name) {
this.name = name;
}
public void sayMyName() {
System.out.println("我的名字是" + name);
}
}
public class Cat extends Animal {
// 创建 Cat 实例时,会先创建 Animal 实例
// 所以要给父类的构造器 super 中传递 new Cat 时的参数
public Cat(String name) {
super(name);
}
// ...
}
3. 实例方法的 Override
又称为覆盖/重写,发生在继承时对父类方法的覆盖,且返回类型、方法名以及参数列表保持一致才算是覆盖。
注意和重载(Overload)的区别,重载是在同一个类或者子类与父类中定义名字相同,但参数不同的方法(两同一不同),可以实现方法的默认值等功能,这里有提到重载。
永远使用@Override
注解来防止手残,善用 IDEA 中的提示。
在 Java 中随处可见 Override,比如String.equals
。
public class User {
private Integer id;
private String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public static void main(String[] args) {
System.out.println(new User(1, "user1") == new User(1, "user1"));
System.out.println(new User(1, "user1").equals(new User(1, "user1")));
System.out.println(new User(1, "user1").equals(new User(2, "user2")));
}
// 请在这里覆盖equals方法,使得两个相同ID的用户equals返回true
@Override
public boolean equals(Object obj) {
if (obj instanceof User) {
return this.id.equals(((User)obj).id);
} else {
return false;
}
}
}
4. 设计模式实战:模板方法模式
提供一个“模板”类,使用可以覆盖模板的全部或部分。
如下所示,Story
是模板,MonsterStory
继承自前者,属于模板的消费者:
public class MonsterStory extends Story {
// 请补全本类,使得main方法可以输出以下内容:
//
// 开始讲故事啦
// 从前有个老妖怪
// 故事讲完啦
// 你还想听吗
public static void main(String[] args) {
new MonsterStory().tellStory();
}
@Override
public void story() {
System.out.println("从前有个老妖怪");
}
@Override
public void endStory() {
super.endStory(); // 部分覆盖时,先保留父类的实现
System.out.println("你还想听吗"); // 再在此基础上增加自己的实现
}
}
public class Story {
public final void tellStory() { // final 定义的方法可以防止被 Override
startStory();
story();
endStory();
}
public void startStory() {
System.out.println("开始讲故事啦");
}
public void story() {
System.out.println("从前有个老和尚");
}
public void endStory() {
System.out.println("故事讲完啦");
}
public static void main(String[] args) {
new Story().tellStory();
}
}
5. 向上/向下转型
- 一个子类类型的对象永远也是一个父类类型的对象
- instanceof 判断类型
- null instanceof ??? 永远是 false
- null 永远可以赋值给一个对象
- 因此,当需要一个父类型时,总可以传递一个子类型
- 但是有时必须进行一些转型,转型是不安全的,所以一定要使用进行强制类型转换:
long i = a;
int b = (int) i;
6. final关键字
在 Java 中,声明类、变量和方法(和方法的参数)时,可使用关键字 final
来修饰,查一下单词,final
代表 最终的
、决定性的
、不可更改的
,所以特点如下:
-
final
修饰的变量是常量,初始化后不能再被赋值(非static成员变量可以晚点再初始化); -
final
修饰的方法不能被子类 Override; -
final
修饰的类不能被继承(因为继承提供了灵活也带来隐患); - 常量的命名约定是全大写加下划线;
-
final
声明的东西不可变,所以编译时会进行一定优化,比如内联到字节码中,可以提高性能。
7. 设计模式实战:单例模式(Singleton pattern)
单例模式限制类的实例化和个数,提供全局的方法来获取唯一实例。
public class World {
// 创建唯一的实例对象
private static final World SINGLETON_INSTANCE = new World();
// 将构造器私有化,这样该类就不会(轻易)再被实例化
private World() {
}
// 暴露出获取可用对象的方法(就像工厂模式一样)
public static World getInstance() {
return SINGLETON_INSTANCE;
}
}
网友评论