面向对象
面向过程思想,把关注点放在一件事或一个活动中涉及到的步骤(也就是过程)上的思想
面向对象,把关注点放在一件事或一个活动中涉及到的人或者事物(也就是对象)上的思想
面向对象的三大特征:
- 封装(encapsulation)
- 继承(inheritance)
- 多态(polymorphism)
类与对象
Java
中通过 “类” 来描述事物,类主要由属性和行为构成
对象,某一类事物的某个具体的存在
类是属性和行为的集合,是一个抽象的概念
对象是该类事物的具体体现,是一种具体的存在
类的生命周期
- 加载(Loading),java 源文件编译成 class 文件,jvm 将 class 文件读入内存,放入方法区
- 连接(Linking):验证(Verification) → 准备(Preparation) → 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
加载、链接、初始化,三个阶段就是类加载阶段 Class Loading
类的定义与使用
定义类的过程,就是把一系列相关事物共同的属性和行为抽取出来的过程;
事物的属性,在类中叫做 成员变量
事物的行为,在类中叫做 成员方法
创建与使用对象:
类名 对象名 = new 类名();
对象名.变量名
对象名.方法名(...)
注意:
成员变量 : 定义在类中、方法外
成员方法 :去掉static
修饰符的方法
自定义对象作为方法的参数时,其和数组作为方法的参数的道理是一样的,传递的是参数的内存地址
成员变量与局部变量的区别
-
成员变量: 有默认初始值
定义位置: 类中、方法外
作用范围: 类中
内存中位置: 堆内存
生命周期:随对象的创建而存在,随对象的消失而消失
-
局部变量: 无默认初始值,必须先赋值再使用
定义位置: 方法中,或者形式参数
作用范围: 方法中
内存中位置: 栈内存
生命周期:随方法的调用而存在,随方法调用的结束而消失
-
Java
中使用变量的规则:使用变量(变量重名)遵循 “就近原则”,如果局部位置有,就使用;没有就去本类的成员位置寻找,有就使用,没有就去父类中寻找,找到就使用,否则报错
不同数据类型的默认值:
boolean:false
、byte:0
、short:0
、char:
、int:0
、long:0
、float:0.0
、double:0.0
、String:null
、String[]:null
char
类型变量后面什么也没有输出。不过,这并不是char
类型变量没有默认值,而是 默认值为 “空字符”,也就是‘\u0000’
,数值为0
证明见下面代码
public class CharDefaultValue {
static char c;
public static void main(String[] args) {
System.out.println((int) c); // 0
System.out.println(c == '\u0000'); // true
}
}
构造函数
java 构造函数,也叫构造方法,是 java 中的一种特殊函数,用来初始化对象
构造函数没有返回类型,函数名和类名保持一致
new
对象产生后,在内存中开辟空间,然后使用构造方法(构造器)完成对象的初始化工作
格式:
// 1
修饰符 类名(参数列表) {
}
// 2
直接类名 (参数列表) {
}
- 构造函数可以调用构造函数
- 方法名必须与类名相同
- 没有返回值
- 构造函数中可以有
return
关键字,但是不能有具体的返回值类型
注意:
- 如果没有提供任何构造方法,系统会给出默认无参构造
- 如果已经提供任何构造方法,系统不再提供无参构造
- 构造方法可以重载
- 构造函数不是手动调用的,而是对象被创建时,jvm 调用的
符合
JavaBean
标准的类,必须是具体的、公共的,并且具有无参数的构造方法,提供用来操作成员变量的set
和get
方法
匿名构造块
构造代码块的格式:
{ }
代码块的作用:对象统一初始化
在对象创建之前,都会执行这个代码块
构造函数重载
构造函数重载是多态的一个典型特例
类中有多个构造函数,参数列表不同
重载构造函数可以实现对象的多种初始化行为
访问权限
在 Java 中,针对类、成员方法和属性提供了四种访问级别,分别是 private
、default
、protected
和 public
-
private
(当前类访问级别):对于私有成员变量和方法,只有在本类中创建该类的对象时,这个对象才能访问自己的私有成员变量和类中的私有方法 -
default
(包访问级别): 类的成员变量和方法什么修饰符都没有,又叫包修饰符,只有类本身成员和当前包下类的成员可以访问 -
protected
(子类访问级别): 用protected
修饰的成员变量和方法能被该类的成员以及其子类成员访问,还可以被同一个包中的其他类的成员访问 -
public
(公共访问级别): 无论访问类和被访问类是否在同一个包中,都可以访问
本类 | 本包 | 子类 | 其他类 | |
---|---|---|---|---|
private | √ | |||
默认 | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
用法:
private 数据类型 变量名;
private 返回值类型 方法名(参数列表){}
案例:
A:给成员变量添加 private
修饰后,测试类中将不能直接访问
B:由于 private
的特性,需要在 Student
类中添加访问该属性的方法,供其它类调用
C:属性的操作一般都是取值和赋值,所以添加对应的公共方法: getXxx()
、setXxx(参数)
D:在测试类中通过 getXxx()
和 setXxx(参数)
方法来实现属性的访问
public static void main(String[] args) {
Student2 stu = new Student2();
stu.setAge(20);
stu.setName("twe");
System.out.println(stu.getAge());
System.out.println(stu.getName());
stu.study();
}
封装
封装,可以提高安全性、复用性,并将复杂的事物简单化
Java
中的封装
将一系列相关事物的共同属性和行为提取出来,放到一个类中,隐藏对象的属性和实现细节,仅对外提供公共的访问方式
隐藏对象数据的实现细节,意味着一个类可以全面地改变存储数据的方式,只要使用同样的方式操作数据
其他对象就不会知道或介意所发生的变化
- 封装原则
- 将不需要对外提供的内容都隐藏起来
- 隐藏属性,但是提供公共方法对其访问
封装的关键
绝对不能让类中的方法直接访问其他类的数据(属性),程序仅通过对象的方法与对象的数据进行交互
-
方法的封装性:
将繁多的代码整合在一起,以一个方法的形式呈现 -
方法的安全性:
调用者不需要知道方法的具体实现就可以使用 -
方法的复用性:
方法可以被重复使用 -
方法的简单化:
繁多的代码以一个方法的形式呈现,仅通过调用方法就可以实现功能,代码维护也变得简单 -
类的封装性:
现实事物的属性和行为都包含在类中 -
类的安全性:
成员变量和成员方法的私有化 -
类的复用性:
类的对象可以被重复使用 -
类的简单化:
类的对象包含了更多的功能,使用起来更方便
封装好处
提高了数据访问的安全性
隐藏了实现细节
应该禁止直接访问一个对象中数据的实际表示,而通过操作方法来访问,这称为数据的隐藏
程序设计追求: 高内聚、低耦合
高内聚: 类的内部数据操作细节自己完成,不允许外部干涉
低耦合: 仅暴露方法给外部使用
继承
Java
中的继承
Java
中的继承,指的是通过扩展一个类来建立另一个类的过程;也就是让类与类之间产生父子关系
Java
中所有的类都直接或间接地继承自:java.lang.Object
;
因此,每一个类都有 toString()
,equals()
,hashCode()
方法
toString()
: 返回对象的字符串表示形式
equals()
: 用于判断两个对象是否相同,如果没有重写,等价于 ==
hashCode()
:返回对象的哈希码
被继承的类叫做父类(基类、超类);
继承的类叫做子类(派生类)
格式:
class 父类 {
// ...
}
class 子类 extends 父类 {
// ...
}
子类可以继承父类的 非私有 成员(成员变量、成员方法)
package cn.itcast.demo;
public class Parent {
// 成员变量
private String name;
private int age;
// 构造方法
public Parent() {
}
public Parent(String name, int age) {
this.name = name;
this.age = age;
}
// setXXX()
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
// getXXX()
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Child extends Parent {
}
public class ObjectOrientedProgramDemo3 {
public static void main(String[] args) {
Child c = new Child();
c.setName("小黑"); // 调用父类方法
// 子类继承父类的非私有成员
// c.age(); // 报错,找不到该属性
System.out.println(c.getName());
}
}
继承的使用场景
子类通过继承,拥有了父类的非私有成员,是开发中的常见做法
继承的使用场景如下:
-
向上抽取:
多个类中存在相同的属性和行为时,可以将这些内容提取出来放到一个新类中,让这些类与新类产生父子关系实现代码复用
-
向下扩展:
当需要扩展已有的类的功能时,可以通过继承已有的类, 在子类中添加新功能或重新实现已有的功能,对父类(和已有的类)没有影响
案例分析:
分别定义 Dog类
、Mouse类
、Pig类
,它们共有的属性有:name
、age
、sex
,共有的行为有:eat()
,Dog类
和 Mouse类
特有的属性为coatColor(毛色)
,三者特有的行为分别是:watch()
,burrow()
,snore()
A:
定义 Dog类
,属性和行为: name
、age
、sex
、coatColor
; eat()
, watch()
B:
定义 Pig类
,属性和行为: name
、age
、sex
; eat()
, snore()
C:
定义 测试类
,分别创建两种动物的对象并使用
D:
抽取 Dog类
和 Pig类
共性内容,定义到 类Animal
中: name
, age
, sex
, eat()
E:
让 Dog类
和 Pig类
继承 Animal类
,删掉重复内容
F:
定义 Mouse类
,继承 Animal类
,特有的属性和行为: burrow()
G:
在 测试类
中创建 Mouse
的对象并使用
案例代码:
package cn.itcast.demo;
public class ObjectOrientedProgramDemo3 {
public static void main(String[] args) {
Dog d = new Dog();
d.eat();
d.watch();
Pig p = new Pig();
p.eat();
p.snore();
}
}
Animal 类:
package cn.itcast.demo;
public abstract class Animal {
// 私有的成员变量
private String name;
private int age;
private String sex;
// 无参构造和全参构造
public Animal() {}
public Animal(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
// set
public void setName(String name) {
this.name = name;
}
public void setSex(String sex) {
this.sex = sex;
}
public void setAge(int age) {
this.age = age;
}
// get
public String getName() {
return this.name;
}
public String getSex() {
return this.sex;
}
public int getAge() {
return this.age;
}
// 成员方法
/* public void eat() {
System.out.println(this.name + " can eat. ");
}*/
// 抽象方法
public abstract void eat();
}
Pig 类 :
package cn.itcast.demo;
public class Pig extends Animal {
// snore
public void snore() {
System.out.println(getName() + " can snore. ");
}
@Override
public void eat() {
System.out.println(getName() + "eat cheese");
}
}
Dog 类:
package cn.itcast.demo;
public class Dog extends Animal {
// watch
public void watch() {
System.out.println(this.getName() + " watch home. ");
}
@Override
public void eat() {
System.out.println(getName() + "eat the bone");
}
}
继承的优缺点
优点
-
功能复用
直接将已有的属性和行为继承过来,可实现功能的复用,可以节省大量的工作
-
便于扩展新功能
在已有功能的基础上,更容易建立、扩展新功能
-
结构清晰,简化认识
同属于一个继承体系的相关类,他们之间的结构层次清晰,简化了人们对于代码结构的认识
-
易维护性
不同类之间的继承关系,让这些事物之间保持了一定程度的一致性,大大降低了维护成本
缺点
-
打破了封装性
父类向子类暴露了实现细节,打破了父类对象的封装性
-
高耦合性
类与类之间紧密的结合在一起,相互依赖性高
程序设计的追求:
低耦合、高内聚耦合:
两个或者更多模块相互依赖于对方
内聚:
模块内部结构紧密,独立性强
继承关系中类成员的使用
当子父类中定义了同名的成员变量
查找变量的原则:就近原则
查找变量的顺序:局部变量 > 成员变量 > 父类 > 更高的父类 > … > Object
访问父类变量的方式:super.父类变量名
super
: 当前对象父类的引用(父类内存空间的标识)
对象初始化顺序:先初始化父类内容,再初始化子类内容
this
和 super
的区别
this
:本质是对象,从本类中开始找
super
:本质是父类内存空间的标识,从父类开始找
当子父类中定义了同名的成员方法
查找方法的原则:就近原则
查找方法的顺序:本类 > 父类 > 更高的父类 > … > Object
访问父类方法的方式:super.父类方法名()
定义重名方法的 前提:
- 父类功能 不能完全满足现实需求,扩展父类功能;
- 父类功能 已经过时,重新实现父类功能
继承关系中子父类构造方法的使用
创建子类对象时,优先调用父类构造方法
子类构造方法的第一行,隐含有语句 super()
,用于调用父类的默认无参构造
在子类创建对象时,必须先初始化该对象的父类内容,若父类中不存在默认无参构造,必须手动调用父类的其他构造
Java
中继承的特点
-
单继承
Java
只支持类的单继承,但是支持多层(重)继承Java
支持接口的多继承,语法为:接口A extends 接口B, 接口C, ...
public class Fruit {
//水果类
}
public class Apple extends Fruit {
//苹果类
}
public class Orange extends Fruit {
//橘子类
}
public class Fuji extends Apple {
//富士苹果类
}
public class GreenApple extends Apple {
//青苹果类
}
-
私有成员不能继承
只能继承父类的非私有成员(成员方法、成员变量)
-
构造方法不能继承
构造方法用于初始化本类对象
创建子类对象时,需要调用父类构造初始化该对象的父类内容;若父类构造可以被继承,该操作则会造成调用的混乱
-
继承体现了
is a
的关系子类符合
is a
父类的情况下,才使用继承,其他情况,不建议使用
多态
多态,多种状态,指同一对象在不同情况下表现出不同的状态或行为
Java 的多态表现在:
- 方法重写
- 方法重载
- 构造函数重载
Java
中实现多态的步骤
-
要有继承(或者实现)关系
-
要有方法重写
-
父类引用指向子类对象(
is a
关系)
public class Test {
public static void main(String [] args) {
// 父类引用指向子类对象
Animal a = new Dog();
}
}
为什么父类引用可以指向子类对象
因为二者满足子类 is a 父类
的关系,所以任何一个Dog
都可以以Animal
的形式使用
当父类引用指向子类对象时:
-
加载类:
创建子类对象时,先加载父类,再加载子类
-
构造方法:
先执行父类的构造方法,初始化子类对象的父类成员部分,然后再初始化子类成员部分
-
成员方法:
类的成员方法在方法区开辟空间,并有一个地址值,该类的每一个对象都会记录方法区中的地址
在类的加载过程中,创建 虚拟方法表,记录了子父类方法重写的信息;通过父类引用调用方法时,会查找虚拟方法表,看该方法是否被重写,如果表中记录了重写信息,则执行子类的重写方法
当父类型变量作为参数时,可以接收任意子类对象
对于子父类中定义的同名的成员变量
成员变量不能重写
成员变量的使用,取决于调用该变量的类型
多态的好处与弊端
多态的好处
-
可维护性
基于继承关系,只需要维护父类代码,提高了代码的复用性,大大降低了维护程序的工作量 -
可扩展性
把不同的子类对象都当作父类看待,屏蔽了不同子类对象间的差异,做出了通用的代码,以适应不同的需求,实现了向后兼容
封装: 隐藏了数据的实现细节,让数据的操作模块化,提高了代码的复用性
继承: 复用方法,从对象的行为这个层面,提高了代码的复用性
多态: 复用对象,程序运行时同一个对象表现出不同的行为
多态的弊端
不能使用子类特有成员
解决办法:
向下转型(前提是,必须准确知道该父类引用指向的子类类型)
类型转换
向上转型
对象向上转型:子类转化为父类
格式:父类名称 对象名称 = new 子类名称();
把创建的子类对象当作父类看待使用,可以使用父类的属性和方法,但是不能使用子类的属性和方法
- 向上转型(自动类型转换)
//子类型转换成父类型
Animal animal = new Dog();
向下转型
对象向下转型:父类转换为子类
格式:子类 引用 = (子类)父类对象 ;
强制类型转换,但是可能会出现异常 ClassCastException
当需要使用子类特有的功能时,需要进行向下类型转换
- 向下类型(强制类型转换)
class Person {
}
class Student extends Person {
}
public class Test {
public static void main(String[] args) {
// 错误
Person p1 = new Person();
Student s1 = (Student) p;
// 正确
Person p2 = new Student();
Student s2 = (Student) p2;
}
}
注意:
只能在继承层次内进行转换,否则可能造成异常(ClassCastException
)
将父类对象转换成子类之前,使用instanceof
进行检查