JavaSE – 面向对象
一、面向对象概述
1.1 面向对象和面向过程
面向过程
:面向过程其实就是面向具体的每一个步骤和过程,也就是面对具体的每一个功能函数,这些功能函数相互调用,完成需求。即就是朝着要实现的功能,一步步的去操作。
面向过程
:面向对象是基于面向过程,对象是将功能进行了封装。只要找到了具体的类,创建出对象,就可以调用其中的具体功能。面向对象也是用来解决问题的一种思维模式。即就是朝着要实现功能的目标,有该功能就调用类。没有的话,就自己设计类。
1.2 面向对象和面向过程差异(面向对象的好处)
面向对象
是一种更符合人们思考习惯的思想面向过程
中更多的体现的是执行者,面向对象
中更多的体现是指挥者。指挥对象做事情。面向对象
将复杂的问题简单化。
1.3 面向对象程序设计
案例:
把大象装进冰箱
针对具体的需求,可以使用名词提炼法
进行分析,寻找具体的对象。
- 要求:把大象装冰箱里
- 对象:大象、冰箱
实施的步骤:
- 打开冰箱门。
- 将大象装进去。
- 关闭冰箱门。
以上步骤有个问题就是:这些行为的发起者是谁???谁来完成???
分析发现打开、装、关闭都是冰箱的功能。即冰箱对象具备如下功能:
- 冰箱打开。
- 冰箱存储。
- 冰箱关闭。
用伪代码描述,上述需求中有两个具体的事物 大象 和 冰箱 。
public class 大象{}// 大象类
public class 冰箱{// 冰箱类
打开(){};
存储(大象){};
关闭(){};
}
当把具体的事物描述清楚之后,需要使用这些具体的事物,Java使用具体的事物,需要通过new关键字
来创建这个事物的具体实例。
// 创建冰箱对象
冰箱 bx = new 冰箱();
// 调用冰箱的功能
对象.功能();
bx.打开();
bx.存储(new 大象());
bx.关闭();
总结:
- 先按照
名词提炼
问题领域中的对象
。 - 对
对象
进行描述,其实就是在明确对象
中应该具备的属性
和功能
。 - 通过
new关键字
就可以创建该事物的具体对象
。 - 通过该
对象
调用它以后的功能。使用.
访问属性
和调用方法()
如: 大象.鼻子
二、类和对象
2.1 类与对象的概念
类
:现实生活中一种事物的抽象,把对事物的描述做成数据也就是属性,把事物具备的功能做成方法。
对象
:对象
(object)代表现实世界中可以明确标识的一个实体。例如:一名学生、一张桌子、一个圆、一个按钮甚至一笔贷款都可以当做是一个对象
。每个对象
都有自己独特的标识
、状态
和行为
。
2.2 类与对象的区别
类
是用于描述现实事物的,它将现实事物进行抽象化,模板化描述。将事物的特点(属性)
和行为
封装在其中。比如小汽车的图纸,图纸就是小汽车的模版。图纸上画着小汽车的各种特点和功能要求。
对象
是现实生活中存在的具体的实例
、个体
。即生活中看到每一个事物,以及我们想象中的事物抽象的概念,都是某一类事物的实例
和个体
。而这些个体都属于某一类事物,即这些个体都是某一类事物中的具体的实例。比如,小汽车就是一类事物,而小汽车又是基于小汽车图纸制造出来的真实个体。因此我们生活中的每一个实物都可以理解为是某一类事物的中的一个个体。创建对象通过对象就可以调用具体的属性
和行为
。
创建对象:new + 构造方法()
构造方法:无返回值,名字与类名相同,功能是配合new关键字来创建对象。
注意:构造方法无返回值因为不需要额外信息数据。与类名一致是因为创建同名对象。
构造方法根据属性的不同有多种重载形式,通常把无参数的构造方法称为无参构造
对象的内存:new + 构造方法() 创建的对象,在堆内存中开辟空间,属性有默认值
2.3 成员变量和局部变量的区别
在内存中的位置不同:
成员变量:成员变量存储在堆内存的对象中
局部变量:局部变量存储在栈内存的方法中
定义的位置不同:
成员变量:定义在类中
局部变量:定义在方法中
生命周期不同:
成员变量:成员变量随着对象的创建而创建,消失而消失
局部变量:局部变量压栈出现,弹栈消失
2.4 匿名对象
当我们的对象只是为了调用某个方法或者作为参数时,使用匿名对象,其实就是不给创建的对象以引用。
class Car
{
String color;// 颜色
int number;// 轮胎数
//描述行为,即方法
public void run()
{
System.out.println(color + ":" + number);
}
}
class CarDemo
{
public static void main(String[] args)
{
//创建Car.class类的对象。
Car c = new Car();
//调用run方法
c.run();
//创建Car.class类的对象。
Car c2 = new Car();
//调用run方法
c2.run();
}
}
匿名对象
的使用场景:当对象对方法进行调用时,而且只调用一次时,可以简化成匿名对象
来书写
Car c = new Car();// 创建对象Car
c.run();// 调用run()方法
// 使用匿名对象并调用run()方法
new Car().run();
匿名对象
也可以作为参数传递:
class CarDemo
{
public static void main(String[] args)
{
Car c = new Car();// 创建Car对象
show(c); //简化成 show(new Car());//把匿名对象作为实际参数进行传递。
}
public static void show(Car cc) // Car cc = c; Car cc = new Car();
{
cc.color = "red";
cc.number = 4;
cc.run();
}
}
三、封装
封装
:是指隐藏对象的属性和实现的细节,仅对外提供公共访问方式。在类定义中用private关键字来实现封装。
封装的特点:
- 隐藏了实现细节
- 提高了代码的复用性
- 提高安全性
private关键字:访问修饰符,代表私有的属性和方法,只能自己访问,对外访问不到
对外访问:private修饰的属性访问不到,就需要提供getXxx()方法和setXxx()方法,即就是对外设置或者获取属性的方法.
构造方法:没有返回值,方法名和类名一致,用来完成初始化,不需要返回值
构造方法不需要私有化,否则无法进行对象的创建.
例如:
public class Demo{
private String name;// 姓名
// 空构造
public Demo(){
}
// 全构造
public Demo(String name){
this.name = name;
}
}
构造方法和一般方法的区别:
- 构造方法只执行一次,一般方法可以多次执行
- 构造方法在初始化时使用,一般方法针对自己需求使用(当需要时进行调用即可使用)
this关键字:代表当前对象,当抽象属性和参数同名时,用this代表当前对象
this调用当前属性:this.属性;
this调用当前方法:this.方法(参数列表);
this调用构造方法:this(参数列表);
构造函数
之间的调用可以通过this关键字
来完成。
// Person类
class Person
{
// Person的成员属性
private int age;
private String name;
// 无参数的构造函数
public Person()
{
}
//给姓名初始化的构造函数
public Person(String name)
{
this.name = name;
}
//给姓名和年龄初始化的构造函数
public Person(String nm , int a)
{
//由于已经存在给姓名进行初始化的构造函数 name = nm;因此只需要调用即可
//调用其他构造函数,需要通过this关键字来调用
this(nm);
//给年龄初始化
age = a;
}
}
构造函数
之间的相互调用可以通过this关键字
完成。
构造函数调用格式:
this(参数列表);
方法的自调用
this.XXX(参数列表)
super
关键字:父类类的引用
四、static
static
静态的,代表的是类级别的属性和方法,是把非私有的属性进行共享使用
static修饰的方法: 直接使用类名.方法()
调用,不需要对象,但是不能调用非静态方法,因为非静态方法的对象还没创建出来.
static修饰的属性: 是类级别的全对象共享,只要有一个对象对其进行操作其他对象单位该静态属性也会随之发生变化
静态方法
使用注意事项:
-
静态方法
不能访问非静态的成员。但是非静态可以访问静态成员
的。说明:
静态
的弊端在于访问出现局限性。好处是可以直接被类名调用。 -
静态方法
中不允许出现this,super关键字。静态不仅可以修饰
方法
,同时静态也可以修饰成员变量
。
// Circle类
class Circle
{
// 圆的半径
private double radius = 10;
// 圆周率,由于圆周率是固定值,因此所有对象共享这个数据
// 没有必要每个对象中拥有这个数据,因此可以使用静态修饰
public static double pi = 3.14;
// 带参数的构造函数
public Circle(double radius)
{
this.radius = radius;
}
// 获取圆面积
public double getArea()
{
return radius * radius * pi;
}
}
// 测试类
class CircleDemo
{
public static void main(String[] args)
{
// 匿名对象调用getArea方法
System.out.println(new Circle(3).getArea());
}
}
如果pi这个变量没有被静态修饰的话,当创建Circle对象时,每个对象中都会有pi这个变量,但是pi是个固定不变的值,没有必要每个对象中拥有,这时可以将这个变量静态修饰,让所有对象共享**就可以了。
静态变量和成员变量的区别
变量所属不同
静态变量
所属于类
,也称为类变量
。
成员变量
所属于对象
,也称为实例变量(对象变量)
。内存中的位置
静态变量
存储于方法区
中的静态区
中。
成员变量
存储于堆内存
中。在内存中出现的时间
静态变量
随着类的加载而加载,随着类的消失而消失。
成员变量
随着对象的创建而在堆内存中出现,随着对象的消失而消失。
五、继承
继承
:使用关键字extends
,让一个类能够访问到一个类的非private属性和方法,提高被继承类的代码复用性。
同名属性
:优先使用自己定义的,在自己的方法中通常有缺省的this来明确调用的是谁的属性,可以使用super关键字来代表父类进一步访问到父类的属性。
同名方法
:发生了方法的重写
继承的好处:
- 提高了代码的复用性,提高软件开发效率
- 让类与类之间有了关系,提供多态的前提
Java中只支持单继承,不支持多继承
但是Java支持多层继承(继承体系)
// 类 -- A
class A{}
// 类 B 继承 A
class B extends A{}
// 类 C 继承 B
class C extends B{}
final
关键字:意为最终,不可变的。可以修饰类,类的成员,以及局部变量
final修饰类: 不可继承
final修饰方法: 不可重写
final修饰变量: 称为常量,有初始值不可以被修改
final修饰的引用变量: 引用不能改,引用的对象的值可以修改
继承-子父类中成员方法特点-重写
当在程序中通过对象调用方法时,会先在子类中查找有没有对应的方法,弱子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法.
【就近原则
】
// 父类
class Fu{
// 方法 -- show()
public void show(){
System.out.println("Fu类中的show方法执行");
}
}
// 子类 继承 父类
class Zi extends Fu{
// 方法 -- show2()
public void show2(){
System.out.println("Zi类中的show2方法执行");
}
}
// 测试类
public class Test{
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
z.show(); //子类中没有show方法,但是可以找到父类方法去执行
z.show2();// 调用子类的show2方法
}
}
成员方法特殊情况——覆盖
子类中出现与父类一模一样的方法时,会出现覆盖操作,也称为override重写
、复写
或者覆盖
。
// 父类
class Fu
{
// 方法 -- show()
public void show()
{
System.out.println("Fu show");
}
}
// 子类 继承 父类
class Zi extends Fu
{
//子类复写了父类的show方法
public void show()
{
System.out.println("Zi show");
}
}
子父类中构造函数特点(super())
在创建子类对象时,父类的构造函数
会先执行,因为子类中所有构造函数
的第一行有默认的隐式super()语句,调用本类中的构造函数
用this(实参列表)语句,调用父类中的构造函数
用super(实参列表)。
为什么子类对象初始化都要访问父类中的构造函数
?因为子类继承
了父类的内容,所以创建对象时必须要先看父类是如何对内容进行初始化的。
public class Test {
public static void main(String[] args) {
new Zi();
}
}
// 父类
class Fu{
int num ;
public Fu(){
System.out.println("Fu构造函数"+num);
num = 4;
}
}
// 子类 继承 父类
class Zi extends Fu{
public Zi(){
System.out.println("Zi构造函数"+num);
}
}
// 执行结果:
// Fu构造函数0
// Zi构造函数4
子类中的构造函数为什么有一句隐式的super()
呢?
原因:子类会继承
父类中的内容,所以子类在初始化时,必须先到父类中去执行父类的初始化动作
。才可以更方便的使用父类中的内容。
当父类中没有空参数构造函数
时,子类的构造函数必须有显示的super()语句指定要访问的父类中的构造函数
。
class Fu extends Object
{
// 无参构造
public Fu()
{
//super();
//显示初始化。
System.out.println("fu constructor run..A..");
}
// 有参构造
public Fu(int x)
{
//显示初始化。
System.out.println("fu constructor run..B.."+x);
}
}
class Zi extends Fu
{
// 无参构造
public Zi()
{
//super();
System.out.println("zi constructor run..C..");
}
// 有参构造
public Zi(int x)
{
super();
System.out.println("zi constructor run..D.."+x);
}
}
// 测试类
class Demo
{
public static void main(String[] args)
{
// 匿名对象
new Zi();
new Zi(3);
}
}
六、抽象类
分析事物时,进行向上抽取,抽取他们的共同属性(最能体现继承的特性)。例如:
// 抽象类 -- quanKe
abstract class quanKe
{
abstract void haoJiao();//抽象函数。需要abstract修饰,并分号;结束
}
// 类 -- Dog 继承 quanKe
class Dog extends quanKe
{
// 对抽象类犬科中的方法进行重写
public void haoJiao()
{
System.out.println("汪汪汪汪");
}
}
// 类 -- Wolf 继承 quanKe
class Wolf extends 犬科
{
// 对抽象类犬科中的方法进行重写
public void haoJiao()
{
System.out.println("嗷嗷嗷嗷");
}
}
关键字:abstract
abstract
:意为抽象,可以修饰类和方法。修饰类的时候不能实例化,修饰方法的时候没有方法体{ }.
注意:
- absract和finnal :final修饰的类不可以被继承,abstract修饰的类必须被继承
- abstract和static: static修饰的方法可以直接通过类调用,abstract修饰的方法不能被调用
- abstract和private:private不能被继承,abstract必须被继承或者必须被重写
七、接口
接口:
是一个极度的抽象类,里面全是抽象方法,接口中可以定义变量,但是变量必须有固定的修饰符修饰,也就是用定义常量的方法定义接口中的变量。
关键字:interface
接口中成员的特点:
1、接口
中可以定义变量,但是变量必须有固定的修饰符修饰,public static final所以接口中的变量也称之为常量
。
2、接口
中可以定义方法,方法也有固定的修饰符,public abstract。
3、接口
中的成员都是公共的。
4、接口
不可以创建对象。
5、子类必须覆盖掉接口
中所有的抽象方法后,子类才可以实例化
。否则子类是一个抽象类。
interface Demo// 定义一个名称为Demo的接口。
{
public static final int NUM = 3;
// 抽象方法
public abstract void show1();
public abstract void show2();
}
// 定义子类去覆盖接口中的方法。子类必须和接口产生关系,类与类的关系是继承,类与接口之间的关系是 实现。通过 关键字 implements
class DemoImpl implements Demo// 子类实现Demo接口。
{
// 重写接口中的方法。
public void show1(){}
public void show2(){}
}
接口-多实现
接口最重要的体现: 解决多继承的弊端。将多继承这种机制在java中通过多实现完成了。
// 接口A
interface A
{
// 抽象方法 -- show1()
public void abstract show1();
}
// 接口B
interface B
{
// 抽象方法 -- show2()
public void abstract show2();
}
// 类C 实现 接口A,B
class C implements A,B// 多实现。同时实现多个接口。
{
// 重写接口A,B中的抽象方法show1,show2
public void show1(){}
public void show2(){}
}
解决多继承的弊端
弊端:多继承时,当多个父类中有相同功能时,子类调用会产生不确定性。
其实核心原因就是在于多继承父类中功能有主体,而导致调用运行时,不确定运行哪个主体内容。
而在多实现里因为接口中的功能都没有方法体,由子类来明确,来源确定就是实现他们的实现类。
子类继承类同时实现接口
子类通过继承父类扩展功能,通过继承扩展的功能都是子类应该具备的基础功能。如果子类想要继续扩展其他类中的功能呢?这时通过实现接口来完成。
// 父类
class Fu
{
// 方法 -- show()
public void show(){}
}
// 接口 -- Inter
interface Inter
{
// 抽象方法 -- show1()
public abstract void show1();
}
// 子类 继承 父类 实现 接口Inter
class Zi extends Fu implements Inter
{
// 重写接口中的抽象方法show1()
public void show1(){}
}
接口的多继承
多个接口之间可以使用extends
进行继承
。
// 接口A
interface A{
// 抽象方法 -- show()
public abstract void show();
}
// 接口B
interface B{
// 抽象方法 -- show1()
public abstract void show1();
}
// 接口C
interface C{
// 抽象方法 -- show2()
public abstract void show2();
}
// 接口D 继承 接口A,B,C
interface D extends A,B,C{
// 抽象方法 -- show3()
public abstract void show3();
}
在开发中如果多个接口中存在相同方法,这时若有个类实现了这些接口,那么就要实现所有接口中的方法,由于接口中的方法是抽象方法,子类实现后也不会发生调用的不确定性。
没有抽象方法的抽象类的由来
在开发中,若一个接口
中有多个抽象方法,但是实现这个接口只使用其中某些方法时,这时我们仍然需要将其他不使用的方法实现,这样明显不符合我们的要求,但不实现这些方法又不行。那么怎么办呢?可以使用一个抽象类
,作为过渡,而这个抽象类
实现这个接口
,所有方法都以空实现存在。这就是没有抽象方法的抽象类的存在价值。我们只要继承这个抽象类,覆盖其中需要使用的方法即可。
//拥有多个方法的接口
interface Inter{
// 抽象方法
public abstract void show();
public abstract void show1();
public abstract void show2();
public abstract void show3();
}
//作为过度的抽象类,此类将接口的所有方法都实现,这里的实现是空实现
abstract class AbsInter implements Inter{
// 重写接口中的抽象方法
public void show(){}
public void show1(){}
public void show2(){}
public void show3(){}
}
/*
//此类直接实现Inter,但只使用其他show和show2方法,这样导致其他两个方法也要实现
//不符合我们的要求
class SubInter2 implements Inter{
public void show() {
System.out.println("show");
}
public void show1() {
}
public void show2() {
System.out.println("show2");
}
public void show3() {
}
}
*/
//此类只使用接口中的show和show2方法,只要覆盖抽象类中的show和show2即可
class SubInter extends AbsInter{
public void show(){
System.out.println("show");
}
public void show2(){
System.out.println("show2")
}
}
接口的好处:
1. 接口的出现扩展了功能
2. 接口其实就是暴露出来的规则
3. 接口的出现降低了耦合性,即设备与设备之间实现了解耦
接口与抽象类的区别:
相同点:
- 都位于继承的顶端,用于被其他
实现
或继承
; - 都不能实例化;
- 都包含
抽象方法
,其子类都必须覆写这些抽象方法;
区别:
抽象类
为部分方法提供实现,避免子类重复实现这些方法,提供代码重用性;接口只能包含抽象方法
,极度的抽象类
;- 一个类只能
继承
一个直接父类(可能是抽象类),却可以实现多个接口;接口弥补了Java的单继承的不足。
二者的选用:
- 优先选用
接口
,尽量少用抽象类
; - 需要定义子类的行为,又要为子类提供共性功能时才选用
抽象类
;
八、多态
多态的体现: 父类的引用或者接口的引用指向了自己的子类对象
Dog d = new Dog();// Dog对象的类型是Dog类型。
Animal a = new Dog();// Dog对象的类型右边是Dog类型,左边Animal类型。
多态的好处:
提高了程序的扩展性。
多态的弊端:
通过父类引用操作子类对象时,只能使用父类中已有的方法,不能操作子类特有的方法。
多态的前提:
- 必须有关系:
继承,实现
。 - 通常都有
重写
操作
多态-转型
当父类的引用 指向子类对象时,就发生了向上转型
,即把子类类型对象转成了父类类型。向上转型的好处是隐藏
了子类类型,提高了代码的扩展性
。
但向上转型也有弊端
,只能使用父类共性的内容,而无法使用子类特有功能,功能有限制。
// 描述动物这种事物的共性eat
abstract class Animal{
// 抽象方法 -- eat()
public abstract void eat();
}
// 类Dog 继承 Animal
class Dog extends Animal{
// 重写Animal中的抽象方法eat()
public void eat(){
System.out.println("啃骨头");
}
// Dog特有的方法lookHome()
public void lookHome(){
System.out.println("看家");
}
}
// 类Cat 继承 Animal
class Cat extends Animal{
// 重写Animal中的抽象方法eat()
public void eat(){
System.out.println("吃鱼");
}
// Cat特有的方法CatchMouse
public void catchMouse(){
System.out.println("抓老鼠");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); //这里形成了多态
a.eat();
//a.lookHome();
//使用Dog特有的方法,需要向下转型
Dog d = (Dog)a;
d.lookHome();
Animal a1 = new Cat();
a1.eat();
/**
* 由于a1具体指向的是Cat的实例,而不是Dog实例,这时将a1强制转成Dog类型,
* 将会发生 ClassCastException 异常,在转之前需要做健壮性判断。
if(!Dog instanceof a1){ // 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;// 方法执行中止
}
Dog d1 = (Dog)a1;
d1.catchMouse();
*/
}
}
向上转型:
不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作,这时就可以使用向上转型
。
向下转型:
当要使用子类特有功能时,就需要使用向下转型
好处:可以使用子类特有功能。
弊端:需要面对具体的子类对象;在向下转型时容易发生ClassCastException类型转换异常。在转换之前必须做类型判断。
多态 - 成员的特点
1. 子父类中出现同名的成员变量
时,多态调用该变量时:
编译时期
:参考的是所属的类的父类中是否有被调用的成员变量
。没有,编译失败。
运行时期
:参考的是所属的类的父类中的成员变量
的值。
简单记
:编译
和运行
都参考等号的左边。编译运行
看左边。
2. 多态成员函数:
编译时期
:参考子类对象
的父类中没有该调用的函数,没有,编译失败。
运行时期
:参考子类有没有重写该方法,并运行子类对象
的成员函数
。
简而言之
:编译
看左边,运行看右边。
3. 多态静态函数:
多态调用:编译
和运行
都参考引用类型变量所属的类中的静态函数
。
简而言之:编译
和运行
看等号的左边。其实真正调用静态方法是不需要对象的,静态方法
通类直接调用。
结论:
- 对于
成员变量
和静态函数
,编译
和运行
都看左边。 - 对于
成员函数
,编译
看左边,运行
看右边。
九、Object
Object
类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object
。
Object类中常用的介绍
1. equals方法
equals方法,用于比较两个对象是否相同,它其实就是使用对象的内存地址在比较。Object类中的equals方法内部使用的就是比较运算符。
在开发中要比较两个对象是否相同,经常会根据对象中的特有数据进行比较,也就是在开发经常需要复写equals方法根据对象的特有数据进行比较。
/*
* 描述人这个类,并定义功能根据年龄判断是否是同龄人
* 由于要根据指定类的特有数据进行比较,这时只要覆盖Object中的equals方法
* 在方法体中根据类的特有数据进行比较
*/
class Person extends Object{
int age ;
//复写父类的equals方法,实现自己的比较方式
public boolean equals(Object obj) {
//判断当前调用equals方法的对象和传递进来的对象是否是同一个
if(this == obj){
return true;
}
//判断传递进来的对象是否是Person类型
if(!(obj instanceof Person)){
return false;
}
//将obj向下转型为Perosn引用,调用其特有数据
Person p = (Person)obj;
return this.age == p.age;
}
}
注意:在复写Object中的equals方法时,一定要注意public boolean equals(Object obj)的参数是Object类型,在调用对象特有数据时,一定要进行类型转换,在转换之前必须进行类型判断。
2. toString方法
toString()
方法返回该对象的字符串表示,其实就是对象的类型+@+哈希值
由于toString()方法返回的结果是内存地址,在开发中经常需要按照对象的特定数据得到相应的表现形式,因此也需要复写它。
class Person extends Object{
int age ;
//根据Person类的特有数据复写toString方法
public String toString() {
return "Person [age=" + age + "]";
}
}