面向对象
- 面向对象编程(Object - Oriented Programming / OOP)
- 面向对象编程的本质:以类的方式组织代码,以对象的方式封装数据
- 三大特性:封装、继承、多态
从认识论角度考虑是先有对象后有类。对象是具体的事物。类是抽象的。
从代码的角度来考虑是现有类再有对象,类是对象的模板。
面向过程 & 面向对象
- 面像过程思想
- 步骤清晰简单,第一步做什么,第二步做什么。。。
- 面向过程适合处理一些较为简单的问题
- 面向对象思想
- 以 分类 的思维模式解决问题,先思考解决问题需要哪些分类,然后进行单独思考。最后才对某个分类下的细节进行面向过程的思考。
- 面向对象适合处理复杂的问题,适合处理需要多人协作的问题
对于描述复杂的事物,从宏观、整体上合理分析,需要用到面向对象的思考方式来分析整个系统,但是具体到细节上,任然需要用到面向过程的思考方式来处理
类与对象
- 类是一种抽象的东西,是定义某一类东西的东西,但并不代表某一个具体的事物
- 例如:家具,食物,餐具......
- 类更像是制造一个对象的图纸
- 对象是类的具体实例
- 例如:椅子,炸鸡,筷子......
- 对象是一个具体的实例,有自己的特点,不是某一个种类,单独的一个个体,不是抽象的
创建与初始化对象
类
-
类中的构造器\构造方法,构造器在创建对象时会调用。
- 类中只有,属性 和 方法
- 特点一:必须和类的名字相同
- 特点二:一定,必须没有返回值,但也不能用void。
public class Student { // 属性 String name; // 未赋值默认为null int age; // 未赋值默认为0 //方法 public void study(){ System.out.println(this.name + "正在干饭......"); // this代表当前这个类 } }
对象
-
使用
new
关键字创建对象- 使用
new
关键字创建对象的时候,会给这个对象分配空间 - 创建好的对象会进行默认初始化,以及对类中构造器的调用
- 实例化一个对象的过程就像,类是玩具的图纸,通过这个图纸(类)拼出来一个玩具(对象)
public class Application { public static void main(String[] args) { // 实例化一个之前定义Student类的一个对象 Student xiaoming = new Student(); // xiaoming这个对象就是一个Student类具体的实例 xiaoming.name = "小明"; // 给xiaoming的name属性赋值 System.out.println(xiaoming.name); System.out.println(xiaoming.age); } }
- 使用
构造器
什么是构造器?
构造器通常也叫构造方法、构造函数、缺省构造器,构造器在
new
对象的时候会被调用,可以完成对象的创建同时给变量赋值使用new关键字,本质是在调用构造器
语法
public 类名 (参数列表,可以没有参数) {
// 不能有return,返回值为空
}
- 必须和类的名字相同
- 一定,必须没有返回值,但也不能用void
- 当一个类没有构造方法时,系统默认提供无参构造
例:
- 创建一个空的
Person
类
public class Person { } // 里面什么都不定义
- 发现能实例化出来这个
Person
类
public class Application {
public static void main(String[] args) {
Person person = new Person();
}
}
- 说明
Person
类里存在一个空的构造器(无参构造器)
public class Person {
public Person() {
// 空的构造器
}
}
- 结论:一个类里面即使什么都不写,也会存在一个方法(无参构造器)
显示的定义一个无参构造器
public class Person {
public Person() {
// 无参构造器
}
}
-
无参构造器能实例化初始值
public class Person { String name; public Person() { // 实例化name的初始值 this.name = "小明"; } }
public class Application { public static void main(String[] args) { // 实例化一个类 Person person = new Person(); // 输出person类的name属性 System.out.printf("name的值为:" + person.name); } }
结果:
name的值为:小明
有参构造器
- 一旦定义有参构造器就一定得显示的定义一个无参构造器
public class Person {
String name;
// 无参构造
public Person() { }
// 有参构造
public Person(String name) {
this.name = name;
}
}
-
在使用
Person
这个类创建对象时会出现方法重载public class Application { public static void main(String[] args) { // 实例化对象的时候传入值 Person person = new Person("小红"); System.out.printf("值为:" + person.name); } }
结果:w
name的值为:小红
因为使用这个类时有值传入,所以程序就往有参构造那走了
封装
在面向对象程式设计方法中,封装(Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。
也就是隐藏内部的实现细节,外部不能查看,通过提供对外的方法来调用与修改。
封装能让代码更易于理解与维护,同时也增强了代码的安全性。
- 封装(数据的隐藏)
- 程序设计追求 “高内聚,低耦合”
- 高内聚:类的内部数据操作细节自己完成,不允许外部干涉
- 低耦合:仅暴露少量的方法给外部使用
- 简单来说就两个东西:
get
和set
- 所有实例相关的东西都是用
引用.
来访问 - 所有静态相关的东西都是用
类名.
来访问
实现封装
- 通过
private
关键字设置为私有属性,再设置公共的 get、set 方法方便外部取得、设置name的值。
public class Student {
// 私有的名字变量
private String name;
// 取得名字的值
public String getName() {
return this.name;
}
// 设置名字的值
public void setName(String name) {
this.name = name;
}
}
-
封装还可以进行判断输入值是否合法,或者更多骚操作。
-
实例:
public class Student { // 私有的名字变量 private String name; // 私有的性别变量 private char sex; // 取得名字的值 public String getName() { return this.name; } // 设置名字的值 public void setName(String name) { this.name = name; } // 取得sex public char getSex() { return sex; } // 设置sex过程中加入判断,防止输入不合法的值 public void setSex(char sex) { if (sex == '男' || sex == '女') { this.sex = sex; }else { System.out.println("输入不合法!"); } } }
调用:
import com.wnaoii.oop.Demo03.Student; public class Application { public static void main(String[] args) { Student student = new Student(); student.setName("小明"); System.out.println("名字叫:" + student.getName()); System.out.println("--------------"); // 输入值不合法 student.setSex('爬'); System.out.println(student.getSex()); // 输入值合法 student.setSex('男'); System.out.println("性别为:" + student.getSex()); } }
结果:
名字叫:小明 -------------- 输入不合法! 性别为:男
继承
- B类继承A类,则A类是B类的超类(superclass)、父类、基类,B类是A类的子类(subclass)、派生类、扩展类
- 子类继承父类,且拥有父类的实例域和方法,但构造方法不继承,私有属性不能在子类中直接访问(通过
super.
关键字来访问) - 继承能增加代码的重复利用率
- Java中只有单继承,没有多继承(一个孩子一个爹)
- 所有类都继承至一个
Object
类,它是所有类的祖宗 - 继承的缺点:耦合度高,父类一旦修改,所有的子类全都受到牵连
类的继承格式
Java中通过 extends
关键字声明
class 父类 {
}
class 子类 extends 父类 {
}
Objext
类是个特殊类,我称之为祖宗类,所有类都默认继承Object
类。- 子类具有父类所有字段,之后定义不能和父类重复。
super & this
-
super
关键字:-
代表父类,子类调用父类的字段时,用
super.fileldName
调用。 -
只能在有继承的情况下才能使用
-
构造方法:
this( );
本类的构造器
-
-
this
关键字:- 代表自己,指向自己的引用,用
this.fileldName
调用。 - 没有继承也可以使用
- 构造方法:
super( );
父类的构造器
- 代表自己,指向自己的引用,用
实例:
- 定义一个
Person
类
package com.wnaoii.oop.Demo04;
public class Person {
protected String Name = "父类Name";
private int Age = 13
}
- 再定义一个
Student
类继承Person
类
package com.wnaoii.oop.Demo04;
// 子类
public class Student extends Person {
private String Name = "子类Name";
public void printName(String name) {
System.out.println(name); // 这个 name 调的是形式参数 name
System.out.println(this.Name); // this.name 调的是当前这个类的 Name
System.out.println(super.Name); // super.name 调的是当前这个类父类的 Name
}
}
- 实例化 student 对象查看效果
package com.wnaoii.oop.Demo04;
public class Application {
public static void main(String[] args) {
Student student = new Student();
student.printName("参数name");
}
}
结果:
参数name
子类Name
父类Name
super 注意点
super
调用父类构造方法,必须在构造方法的第一个。super
必须只能出现在子类的方法或构造方法中。super
和 this 不能同时调用构造方法。(因为两者都必须在第一行,有冲突)
protected
- 在继承中,子类无法访问父类的
private
字段或者private
方法。 protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问。
无法调用:
// 父类
public class Person {
// 定义一个 private name变量
private String name = "父类Name";
}
// 子类
public class Student extends Person {
public void printName() {
System.out.println(super.name); // 编译错误,无法调用
}
}
成功调用:
- 由于
private
关键字使得子类无法调用,则可以把private
改为protected
,这样子类便能调用父类被protected
修饰的字段。
// 父类
public class Person {
// 定义一个 protected name变量
protected String name = "父类Name";
}
// 子类
public class Student extends Person {
public void printName() {
System.out.println(super.name); // 子类通过关键字 super 成功调用
}
}
继承中的构造器
- 子类并不继承父类的构造器(构造方法或者构造函数),只是调用(隐式或显式)。
// 父类
public class Person {
// 一个简单的无参构造
public Person() {
System.out.println("已执行Person无参构造!")
}
}
// 子类
public class Student extends Person {
// 此处隐藏了一个调用父类的无参构造
// super();
public Student() {
System.out.println("已执行Student无参构造!")
}
}
// 调用
public class Application {
public static void main(String[] args) {
// 只用 Student 类创建一个对象
Student student = new Student();
}
}
结果:
已执行Person无参构造!
已执行Student无参构造!
当显示调用父类无参构造则必须在子类构造器的第一行。(不写一般都默认调用父类无参构造器)
若父类的构造器带有参数,则必须在子类的构造器中显式地通过
super
关键字调用父类的构造器并配以适当的参数列表。若父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。
方法重写(override)
为什么要重写?
- 父类的功能,子类不一定需要,或子类需要的更多!
特点:
- 子类和父类需要有继承关系
- 重写是对父类可访问的方法的重写,和属性无关
- 声明为 final 的方法不能被重写
- 声明为 static 的方法不能被重写,但是能够被再次声明
- 修饰符的范围可以扩大但不能缩小。(public > protected > Default > private)
- 抛出的异常范围可以被缩小,但不能扩大
- 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类。(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)
- 方法名、返回值、形参必须相同,方法体不同。(外壳不变,核心重写)
例:
package com.wnaoii.oop.Demo05;
public class Person {
public void say() {
System.out.println("Person会说话!");
}
}
package com.wnaoii.oop.Demo05;
public class A extends Person {
@Override
public void say() {
System.out.println("A不但会说,还会唱、跳、Rep!");
}
}
package com.wnaoii.oop;
import com.wnaoii.oop.Demo05.Person;
import com.wnaoii.oop.Demo05.A;
public class Application {
public static void main(String[] args) {
Person person = new Person();
person.say();
A a = new A();
a.say();
}
}
输出:
Person会说话!
A不但会说,还会唱、跳、Rep!
多态
一个类的多种形态
父类型引用指向子类型对象
编译时一种形态,运行时一种形态,这就是多态
编译时多态:通过 overloading(重载) 实现
编译阶段绑定父类型的方法
运行时多态:通过 overriding(重写) 和 继承实现
运行阶段动态绑定子类型对象的方法
多态存在的三个条件
- 拥有继承关系
- 子类重写父类方法
- 父类引用指向子类对象:Parent p = new Child( );
- upcasting 向上转型(父-->子 自动类型转换):将子类对象直接赋值给父类引用
- 只有向上转型过的对象才能向下转型
- downcasting 向下转型(子-->父 强制类型转换):将指向子类对象的父类引用赋值给子类引用
实例:
package com.wnaoii.oop.Demo06;
//父类
public class Person {
public int age = 0;
public void run() {
System.out.println("run");
}
}
和父类有继承关系的Student类:
package com.wnaoii.oop.Demo06;
// 子类
public class Student extends Person {
public int age = 19;
// 重写父类的 run() 方法
@Override
public void run() {
System.out.println("子类run()方法被调用!");
}
// 子类特有的方法
public void eat() {
System.out.println("子类特有eat()方法被调用!");
}
}
调用:
package com.wnaoii.oop;
import com.wnaoii.oop.Demo06.Person;
import com.wnaoii.oop.Demo06.Student;
public class Application {
public static void main(String[] args) {
Student s1 = new Student();
// 父类的引用指向子类的对象(向上转型)
Person s2 = new Student();
// 多态成员方法调用
s1.run(); // 能调用自己的和继承的方法
s1.eat(); // 类型是 Student 正常调用
System.out.println();
s2.run(); // 子类重写了父类的方法,所以优先执行子类的方法
// s2.eat(); s2的类型为 Person ,而Person中没有eat()方法,无法调用
System.out.println();
// 多态成员变量调用
System.out.println(s1.age);
System.out.println(s2.age);
}
}
结果:
子类run()方法被调用!
子类特有eat()方法被调用!
子类run()方法被调用!
19
0
由此可以看出多态成员访问的特点:
成员变量:编译看父类(左边),运行看父类(左边)
在程序的编译阶段,根据父类中的变量进行编译,实际运行时也是根据父类的变量进行运行
成员方法:编译看父类(左边),运行看子类(右边)
在程序的编译阶段,根据父类的方法进行编译,父类有这个方法编译通过,没有则报 ClassCastException
异常(类型转换异常),在实际运行时则运行的是子类中的方法
具体分析
例:有一个Animal
的父类,有一个move()
方法,一个Dog
子类,重写了父类的move
方法
Animal dog = new Dog();
编译阶段:
对于编译器来说,编译器只知道dog
这个对象的类型是Animal
,所以编译器在检查语法的时候只会去Animal.class
字节码文件中找父类的move()
方法,找到了就绑定上move()
方法,编译通过,静态绑定成功!(编译阶段属于静态绑定)
运行阶段:
在运行阶段中,实际上堆内存中创建的java对象是Dog
的对象,所以运行move()
方法时,实际参与的是一只dog
,是对Dog
对象中重写的move()
方法的调用。(运行阶段绑定属于动态绑定)
向上转型
向上转型(Upcasting)是指将子类的引用赋值给父类的变量的过程,又或者说是将一个对象的类型转换为它的父类型或接口类型。这个过程也称为向上转型。这种转型是自动的,并且在大多数情况下都是安全的。
特点:
- 向上转型是安全的:由于父类包含了子类的所有方法和属性,所以向上转型后对象仍然可以使用所有的方法和属性
- 向上转型会丢失子类特有的方法和属性:由于向上转型后的对象只能访问父类的方法和属性,所以子类特有的方法和属性将不再可用
- 向上转型可以使用 instanceof 运算符进行检测:可以使用 instanceof 运算符来检测对象是否是特定类型的实例。这对于在向上转型后确定对象的类型很有用
- 向上转型可以使用强制类型转换进行恢复:如果需要访问子类特有的方法和属性,则可以使用强制类型转换将对象转回原来的子类类型。但是,这需要进行类型检查,以确保转换是安全的
例:假设有一个类型为Dog
的对象,它有一个父类型为Animal
。在这种情况下,可以将Dog
对象向上转型为Animal
类型,如下所示:
Animal animal = new Dog();
在这种情况下,animal
变量仍然指向原来的Dog
对象,但是可以通过animal
变量调用的方法受到限制。例如,如果Dog
类中有一个名为run
的方法,则无法通过animal
变量调用这个方法,因为Animal
类中没有这个方法,这个方法是子类Dog
特有的方法。
向下转型
向下转型(downcasting)是指将一个对象的引用从父类型向子类型转型。因为它可以让我们在不改变对象本身的情况下改变对对象的引用。这种转型不是自动的,且不安全!
特点:
- 只有向上转型过的对象才可以向下转型
- 向下转型是不安全的:由于向下转型后的对象会访问子类特有的方法和属性,所以如果对象本身不是子类的实例,就会发生类型转换异常
- 向下转型可以使用 instanceof 运算符进行检测:可以使用 instanceof 运算符来检测对象是否是特定类型的实例。这对于在向下转型前确定对象的类型很有用
- 向下转型必须使用强制类型转换:必须使用强制类型转换将对象转换为子类类型。但是,这需要进行类型检查,以确保转换是安全的
例:假设有一个类Animal
和它的子类Dog
,并且有一个Animal
类型的对象animal
,我们可以将animal
向下转型为Dog
类型,如下所示:
Animal animal = new Animal();
Dog dog = (Dog)animal
但是,如果animal
对象本身不是Dog
类的实例,animal
是其他类new
实例化出来的对象,在这种情况下,尝试向下转型将会导致类型转换异常ClassCastException
。如下所示:
Animal animal = new cat();
Dog dog = (Dog)animal;
因此,在进行向下转型之前,通常会使用instanceof
运算符来检测对象是否是特定类型的实例,以确保转换是安全的。
instanceof
运算符的使用
可以在运行阶段动态判断引用指向的对象的类型,instanceof
运算符的运算结果只能是布尔类型
规范:任何时候,任何地点,对类型进行向下转型时,一定要使用instanceof
运算符进行判断
语法
引用 instanceof 类型
例1: a 是一个引用, a 变量保存了内存地址指向了堆中的对象
-
a instanceof Animal
运算结果为true
a
引用指向的堆内存中的java对象是一个Animal
-
a instanceof Animal
运算结果为true
: -
a
引用指向的堆内存中的java对象不是一个Animal
例2:父类Animal
,子类1Cat
,子类2Dog
。
Animal a = new cat();
// 将Animal类型的a引用向下转型,Dog类和Cat类都继承自Animal类,但Dog类和Cat类相互并没有继承关系,此时强转就会出现类型转换异常
Dog dog = (Dog)a;
使用instanceof
运算符进行判断
// 向上转型
Animal a = new cat();
// 添加instanceof运算符判断
if (a instanceof Dog) {
// 运算结果为true时才开始向下转型
Dog dog = (Dog)a;
}
什么时候需要使用向下转型?
父类需要访问子类中特有的方法时需要向下转型
多态在开发中的实际应用
多态在开发中的作用:降低程序的耦合度,提高程序的扩展力
编写一个程序模拟主人
喂养宠物
的场景:
提示1:
- 主人类:Master
- 宠物类:Pet
- 宠物类子类:Dog、Cat、fish
提示2:
- 主人应该提供一个喂养的方法:
feed()
- 宠物应该有一个吃的方法:
eat()
要求:主人类中只提供一个feet()
方法,达到喂养各种类型宠物的目的
编写测试程序:
- 创建主人对象
- 创建各种宠物对象
- 调用主人的
feed()
方法喂养不同的宠物,观察执行结果
编写过程:
-
创建主人类
public class Master {}
-
创建宠物类,实现
eat()
方法public class Pet { public void eat() {} }
-
创建宠物类子类,重写父类的
eat()
方法-
Dog类
public class Dog extends Pet{ public void eat() { System.out.println("小狗正在吃骨头!"); } }
-
Cat类
public class Cat extends Pet{ public void eat() { System.out.println("小猫正在吃鱼!"); } }
-
Fish类
public class Fish extends Pet{ public void eat() { System.out.println("小鱼正在吃小虾米!"); } }
-
-
在主人类中添加
feed()
方法实现喂养多种类型的宠物public class Master { // 形参使用各种宠物的父类pet,传参时自动向上转型 public void feed(Pet p){ p.eat(); } }
-
编写测试类开始测试
public class Test { public static void main(String[] args) { // 创建主人对象 Master master = new Master(); // 创建各种宠物对象 Dog dog = new Dog(); Cat cat = new Cat(); Fish fish = new Fish(); // 调用主人的feed()方法喂养不同的宠物 master.feed(dog); master.feed(cat); master.feed(fish); } }
-
输出结果
小狗正在吃骨头! 小猫正在吃鱼! 小鱼正在吃小虾米!