本章重点针对面向对象编程的三大特征:继承、封装、多态进行详细的讲解。另外还包 括抽象类、接口、内部类等概念。很多概念对于初学者来说,更多的是先进行语法性质的了 解。不要期望,通过本章学习就“搞透面向对象编程”。本章只是面向对象编程的起点,后 面所有的章节说白了都是对面向对象这一章的应用。
1 继承
继承是面向对象编程的三大特征之一,它让我们更加容易实现对于已有类的扩展、更加 容易实现对于现实世界的建模。 继承有两个主要作用:
1.代码复用,更加容易实现类的扩展 。
2.方便建模。
1.1 继承的实现
继承让我们更加容易实现类的扩展。 比如,我们定义了人类,再定义 Boy 类就只需要 扩展人类即可。实现了代码的重用,不用再重新发明轮子(don’t reinvent wheels)。 从英文字面意思理解,extends 的意思是“扩展”。子类是父类的扩展。现实世界中的 继承无处不在。比如:
上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果 新定义一个 Student 类,发现已经有 Person类包含了我们需要的属性和方法,那么 Student 类只需要继承 Person 类即可拥有 Person 类的属性和方法。
使用 extends 实现继承的代码如下:
package com.txw.test;
public class Test {
public static void main(String[] args) {
Student student =new Student("Adair",160,"java");
student.rest();
student.study();
}
}
class Person{
String name;
int height;
public void rest(){
System.out.println("休息一会!");
}
}
class Student extends Person{
String major; // 专业
public void study(){
System.out.println("在CSDN学习Java!");
}
public Student( String name, int height,String major) {
// 天然拥有父类的属性
this.name = name;
this.height =height;
this.major = major;
}
}
1.2 instanceof 运算符
instanceof 是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对 象时,返回 true;否则,返回 false。
使用instanceof运算符进行类型判断的代码如下:
package com.txw.test;
public class Test {
public static void main(String[] args) {
Student student =new Student("Adair",160,"java");
System.out.println(student instanceof Person);
System.out.println(student instanceof Student);
}
}
class Person{
String name;
int height;
public void rest(){
System.out.println("休息一会!");
}
}
class Student extends Person{
String major; // 专业
public void study(){
System.out.println("在CSDN学习Java!");
}
public Student( String name, int height,String major) {
// 天然拥有父类的属性
this.name = name;
this.height =height;
this.major = major;
}
}
两条语句的输出结果都是true。
1.3 继承使用要点
1.父类也称作超类、基类。子类:派生类等。
2.Java 中只有单继承,没有像 C++那样的多继承。多继承会引起混乱,使得继承链 过于复杂,系统难于维护。
3.Java 中类没有多继承,接口有多继承。
4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
5.如果定义一个类时,没有调用 extends,则它的父类是:java.lang.Object。
1.4 方法的重写 override
子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态 的必要条件。
方法的重写需要符合下面的三个要点:
1.“= =”: 方法名、形参列表相同。
2.“≤”:返回值类型和声明异常类型,子类小于等于父类。
3.“≥”: 访问权限,子类大于等于父类。
方法重写的代码如下:
package com.txw.test;
public class TestOverride {
public static void main(String[] args) {
Vehicle v1 = new Vehicle();
Vehicle v2 = new Horse();
Vehicle v3 = new Plane();
v1.run();
v2.run();
v3.run();
v2.stop();
v3.stop();
}
}
// 交通工具类
class Vehicle{
public void run(){
System.out.println("跑...");
}
public void stop() {
System.out.println("停止不动");
}
}
// 马也是交通工具
class Horse extends Vehicle{
// 重写父类的方法
public void run() {
System.out.println("四蹄翻飞,嘚嘚嘚...");
}
}
class Plane extends Vehicle {
// 重写父类方法
public void run() {
System.out.println("天上飞!");
}
public void stop() {
System.out.println("空中不能停,坠毁了!");
}
}
1.5 final 关键字
final 关键字的作用:
1.修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
final int MAX_SPEED = 120;
2.修饰方法:该方法不可被子类重写。但是可以被重载!
final void shout(){}
如图所示:
3.修饰类: 修饰的类不能被继承。 比如:Math、String 等。
final class Dog {}
如图所示:
1.6 继承和组合
我们可以通过继承方便的复用已经定义类的代码。还有一种方式,也可以方便的实现“代 码复用”,那就是:“组合”。
“组合”不同于继承,更加灵活。 “组合”的核心就是“将父类对象作为子类的属性”,然后,“子类通过调用这个属性 来获得父类的属性和方法”。
我们把继承第一节课中的代码,使用组合来实现一遍。代码如下:
package com.txw.test;
public class Test{
public static void main(String[] args) {
Student s = new Student("Adair",160,"Java");
s.person.rest(); //s.rest();
s.study();
}
}
class Person {
String name;
int height;
public void rest(){
System.out.println("休息一会!");
}
}
class Student /*extends Person*/ {
Person person = new Person();
String major; // 专业
public void study(){
System.out.println("在CSDN学习Java!");
}
public Student(String name, int height, String major) {
// 天然拥有父类的属性
this.person.name = name; // this.name = name;
this.person.height = height; // this.height = height;
this.person.rest();
this.major = major;
}
}
组合比较灵活。继承只能有一个父类,但是组合可以有多个属性。所以,有人声称“组 合优于继承,开发中可以不用继承”,但是,不建议大家走极端。
继承除了代码复用、也能方便我们对事物建模。所以,对于“is -a”关系建议使用继承, “has-a”关系建议使用组合。
比如:上面的例子,Student is a Person 这个逻辑没问题,但是:Student has a Person 就有问题了。这时候,显然继承关系比较合适。
再比如:笔记本和芯片的关系显然是“has-a”关系,使用组合更好。
2 Object 类详解
我们学习的所有类、我们以后定义的所有类都是 Object 类的子类,也都具备 Object 类的所有特性。因此,我们非常有必要掌握 Object 类的用法。
2.1 Object 类基本特性
Object 类是所有 Java 类的根基类,也就意味着所有的 Java 对象都拥有 Object 类的属 性和方法。如果在类的声明中未使用 extends 关键字指明其父类,则默认继承 Object 类。
Object 类的代码如下:
public class Person {
...
}
// 等价于:
public class Person extends Object {
...
}
补:IDEA 部分快捷键
IDEA 快捷键和相关操作:
1.类的结构视图:alt+7
2.看类的源码:ctrl+左键
3. 查看类的关系:鼠标放到类名上,右键–>Diagram–>show Diagram
4.自动生成构造器、get、set 方法、equals 等:alt+insert
5. 鼠标悬停显示提示(重新设置:File–>Setting–>Editor–>general):
6.快捷输出常见字符串:
a) main public static void main(String[] args){}
b) sout System.out.println();
c) soutm System.out.println(“描述:所在类中的,所在方法”);
2.2 toString 方法
Object 类中定义有 public String toString()方法,其返回值是 String 类型。Object 类中 toString 方法的源码为:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
根据如上源码得知,默认会返回“类名+@+16 进制的 hashcode”。在打印输出或者 用字符串连接对象时,会自动调用该对象的 toString()方法。
重写 toString()方法的代码如下:
package com.txw.test;
class Person{
String name;
int age;
@Override
public String toString() {
return name + ",年龄:" + age;
}
}
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.age = 25;
person.name = "Adair";
System.out.println("info: " + person);
Test test = new Test();
System.out.println(test);
}
}
执行结果如图所示:
2.3 ==和 equals 方法
“==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表 示地址相等即是同一个对象。
Object 类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容 相等”的逻辑。比如,我们在公安系统中认为 id 相同的人就是同一个人、学籍系统中认为 学号相同的人就是同一个人。
Object 的 equals 方法默认就是比较两个对象的 hashcode,是同一个对象的引用时 返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写 equals 方法。
自定义类重写 equals()方法的代码如下:
package com.txw.test;
import java.util.Objects;
class Person{
int id;
String name;
public Person(int id,String name) {
this.id=id;
this.name=name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id && Objects.equals(name, person.name);
}
}
public class TestEquals {
public static void main(String[] args) {
Person p1 = new Person(123,"Adair");
Person p2 = new Person(123,"Adair6");
System.out.println(p1==p2); // false,不是同一个对象
System.out.println(p1.equals(p2)); // false,不是同一个对象
String s1 = new String("学无止路");
String s2 = new String("学无止路");
System.out.println(s1==s2); // false, 两个字符串不是同一个对象
System.out.println(s1.equals(s2)); // true, 两个字符串内容相同
}
}
JDK 提供的一些类,如 String、Date、包装类等,重写了 Object 的 equals 方法,调 用这些类的 equals 方法, x.equals (y) ,当 x 和 y 所引用的对象是同一类对象且属性内容 相等时(并不一定是相同对象),返回 true 否则返回 false。
2.4 super 关键字
super“可以看做”是直接父类对象的引用。可以通过 super 来访问父类中被子类覆盖 的方法或属性。
使用 super 调用普通方法,语句没有位置限制,可以在子类中随便调用。
在一个类中,若是构造方法的第一行代码没有显式的调用 super(…)或者 this(…);那么 Java 默认都会调用 super(),含义是调用父类的无参数构造方法。这里的 super()可以省略。
super 关键字的使用代码如下:
package com.txw.test;
class FatherClass{
public int value;
public void f(){
value = 100;
System.out.println("FatherClass.value" + value);
}
}
class ChildClass extends FatherClass {
public int value;
public void f() {
super.f(); // 调用父类的普通方法
value = 200;
System.out.println("ChildClass.value="+value);
System.out.println(value);
System.out.println(super.value); // 调用父类的成员变量
}
}
public class TestSuper01 {
public static void main(String[] args) {
new ChildClass().f();
}
}
执行结果为如图所示:
2.6 继承树追溯
属性/方法查找顺序:(比如:查找变量 h)
1.查找当前类中有没有属性 h
2.依次上溯每个父类,查看每个父类中是否有 h,直到 Object
3.如果没找到,则出现编译错误。
4.上面步骤,只要找到 h 变量,则这个过程终止。
·构造方法调用顺序:
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上 追溯到 Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。
继承条件下构造方法的执行过程的代码如下:
package com.txw.test;
class FatherClass{
public FatherClass() {
System.out.println("创建FatherClass");
}
}
class ChildClass extends FatherClass {
public ChildClass() {
System.out.println("创建ChildClass");
}
}
public class TestSuper02 {
public static void main(String[] args) {
System.out.println("开始创建一个ChildClass对象......");
new ChildClass();
}
}
执行为如图所示:
3 封装(encapsulation)
封装是面向对象三大特征之一。对于程序合理的封装让外部调用更加方便,更加利于写 作。同时,对于实现者来说也更加容易修正和改版代码。
3.1 封装的作用和含义
我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有 必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只 给我们暴露简单的接口,比如:电源开关。具体内部是怎么实现的,我们不需要操心。
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的 专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部 实现细节。
我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成, 不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
编程中封装的具体优点:
1.提高代码的安全性。
2.提高代码的复用性。
3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
4.“低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。
未进行封装的代码演示:
package com.txw.test;
class Person{
String name;
int age;
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.name ="Adair";
person.age = -12; // 年龄可以通过这种方式随意赋值,没有任何限制
System.out.println(person);
}
}
我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图 所示:
再比如说,如果哪天我们需要将 Person 类中的 age 属性修改为 String 类型的,你会 怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到 了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍 微修改下 Person 类的 setAge()方法即可,而无需修改使用了该类的客户代码。
3.2 封装的实现—使用访问控制符
Java 是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。 Java 中 4 种“访问控制符”分别为 private、default、protected、public,它们说明了面向对 象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。
下面详细讲述它们的访问权限问题。其访问权限范围如表所示。
1.private 表示私有,只有自己类能访问。
2.default 表示没有修饰符修饰,只有同一个包的类能访问。
3.protected 表示可以被同一个包的类以及其他包中的子类访问。
4.public 表示可以被该项目的所有包中的所有类访问。
【注】关于 protected 的两个细节:
1.若父类和子类在同一个包中, 子类可访问父类的 protected 成员,也可访问父类对象 的 protected 成员。
2.若子类和父类不在同一个包中,子类可访问父类的 protected 成员,不能访问父类对象 的 protected 成员。
3.3 封装的使用细节
开发中封装的简单规则:
1.属性一般使用 private 访问权限。
1.1属性私有后, 提供相应的 get/set 方法来访问相关属性,这些方法通常是 public 修饰的,以提供对属性的赋值与读取操作(注意:boolean 变量的 get 方法是 is 开头!)。
2.方法:一些只用于本类的辅助性方法可以用 private 修饰,希望其他类调用的方法 用 public 修饰。
JavaBean 的封装演示:
package com.txw.test;
public class Person {
// 属性一般使用private修饰
private String name;
private int age;
private boolean flag;
// 为属性提供public修饰的set/get方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isFlag() { {// 注意:boolean类型的属性get方法是is开头的
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
封装的使用代码如下:
package com.txw.test;
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
// this.age = age; // 构造方法中不能直接赋值,应该调用setAge方法
setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 在赋值之前先判断年龄是否合法
if (age > 130 || age < 0) {
this.age = 18; // 不合法赋默认值18
} else {
this.age = age; // 合法才能赋值给属性age
}
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test2 {
public static void main(String[] args) {
Person person = new Person();
// person.name = "小红"; // 编译错误
// person.age = -45; // 编译错误
person.setName("Adair");
person.setAge(-12);
System.out.println(person);
Person person1 = new Person("adair",300);
System.out.println(person1);
}
}
执行结果为如图所示:
4 多态(polymorphism)
4.1 多态概念和实现
多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个 方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是 旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人 用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
1.多态是方法的多态,不是属性的多态(多态与属性无关)。
2.多态的存在要有 3 个必要条件:继承,方法重写,父类引用指向子类对象。
3.父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
多态和类型转换的代码如下:
package com.txw.test;
class Animal {
public void shout() {
System.out.println("叫了一声!");
}
}
class Dog extends Animal {
public void shout() {
System.out.println("旺旺旺!");
}
public void seeDoor() {
System.out.println("看门中....");
}
}
class Cat extends Animal {
public void shout() {
System.out.println("喵喵喵喵!");
}
}
public class TestPolym {
public static void main(String[] args) {
Animal a1 = new Cat(); // 向上可以自动转型
// 传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
animalCry(a1);
Animal a2 = new Dog();
animalCry(a2); // a2为编译类型,Dog对象才是运行时类型。
/*
编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
否则通不过编译器的检查。
*/
Dog dog = (Dog)a2; // 向下需要强制类型转换
dog.seeDoor();
}
// 有了多态,只需要让增加的这个类继承Animal类就可以了。
static void animalCry(Animal a){
a.shout();
}
/*
如果没有多态,我们这里需要写很多重载的方法。
每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
static void animalCry(Dog d) {
d.shout();
}
static void animalCry(Cat c) {
c.shout();
}*/
}
执行为如图所示:
给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可 以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。
由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多 态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用 Dog 类特有的 seeDoor()方法。
那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容: 对象的转型。
4.2 对象的转型(casting)
父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方 法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!
对象的转型代码如下:
package com.txw.test;
public class TestCasting {
public static void main(String[] args) {
Object obj = new String("学无止路"); // 向上可以自动转型
// obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
/* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
* 不然通不过编译器的检查。
*/
String str = (String) obj; // 向下转型
System.out.println(str.charAt(0)); // 位于0索引位置的字符
System.out.println(obj == str); // true.他们俩运行时是同一个对象
}
}
执行结果为如图所示:
在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类 型转换异常 ClassCastException。
类型转换异常的代码如下:
package com.txw.test;
public class TestCasting2 {
public static void main(String[] args) {
Object obj = new String("学无止路"); // 向上可以自动转型
// 真实的子类类型是String,但是此处向下转型为StringBuffer
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
执行结果为如图所示:
为了避免出现这种异常,我们可以使用所学的instanceof运算符进行判断。
向下转型中使用 instanceof的代码如下:
package com.txw.test;
public class TestCasting3 {
public static void main(String[] args) {
Object obj = new String("学无止路"); // 向上可以自动转型
if(obj instanceof String){
String str = (String)obj;
System.out.println(str.charAt(0));
}else if(obj instanceof StringBuffer){
StringBuffer str = (StringBuffer) obj;
System.out.println(str.charAt(0));
}
}
}
执行结果为如图所示: