F_JustWei's Studio.

有关面向对象的一些总结

字数统计: 5.5k阅读时长: 20 min
2021/04/24 Share

有关面向对象的一些总结

1、面向过程的思想和面向对象的思想

面向对象和面向过程的思想有着本质上的区别。

对于面向过程的思维来说,当你拿到一个问题时,你分析这个问题应该是第一步先做什么,第二步再做什么。

但对于面向对象的思维来说,第一步你应该分析这个问题里面有哪些类和对象,第二步再分析这些类和对象应该具有哪些属性和方法。。第三步分析类和类之间具体有什么关系。

面向对象有一个非常重要的设计思维:合适的方法应该出现在合适的类里面。

2、面向对象的设计思想

从现实世界中客观存在的事物出发来构造软件系统,并在系统的构造中尽可能运用人类的自然思维方式。

3、类和对象

对象是用于计算机语言对问题中事物的描述,对象通过属性(attribute)和方法(method)来分别对应事物所具有的静态属性和动态属性。

类是用于描述同一类的对象的一个抽象的概念,类中定义了这一类对象所具有的静态属性和动态属性

类可以看成一类对象的模板,对象可以看成该类的一个具体实例

4、封装

利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象。

封装的优点:

  • 减少耦合:可以独立地开发、测试、优化、使用、理解和修改。
  • 减轻维护的负担:可以更容易被理解,并且在调试的时候可以不影响其他模块。
  • 有效地调节性能:可以通过剖析来确定哪些模块影响了系统的性能。
  • 提高软件的可重用性。
  • 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的。

以下 Person 类封装 name、gender、age 等属性,外界只能通过 getter方法获取一个 Person 对象的 name 属性、 gender 属性、 flag 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。

注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Person {

private String name;
private int gender;
private int age
private boolean flag;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public String getGender() {
return gender == 0 ? "man" : "woman";
}
public void setGender(int gender) {
this.gender = gender;
}

public void setAge(int age) {
this.age = age;
}

public boolean isFlag() {
return flag;
}

public void setFlag(boolean flag) {
this.flag = flag;
}

public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}

采用 this 关键字是为了解决实例变量(private String name)和局部变量之间发生的同名的冲突。

封装同时可以提高代码的安全性,例如普通的类属性不是private修饰就直接可以通过”对象名.属性 = xxx”对其赋值,但当我们用private修饰该属性后就不能这样对其做任意的修改了,而且我们还可以在其对外的访问方法中进行合法值校验。

封装的使用细节:

  • 一般使用 private 访问权限。
  • 提供相应的 getter、setter 方法来访问相关属性,这些方法通常是 public 修饰的。以提供对属性的赋值与读取操作。(注意! boolean 变量的 getter 方法是 is 开头。)
  • 一些只用于本类的辅助性方法,可以使用 private 修饰,希望其他类调用的方法用 public 修饰。

5、继承

继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法

继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。

Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型

1
Animal animal = new Cat();
继承的优点:
  • 提高类代码的复用性。

  • 提高了代码的维护性。

  • 使得类和类产生了关系,是多态的前提(它也是继承的一个弊端,类的耦合性提高了)。

继承的特性:
  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法,即重写父类方法。
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
  • 继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承Object(Object在 java.lang 包中,所以不需要 import)。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

6、多态

多态分为编译时多态和运行时多态:

  • 编译时多态主要指方法的重载。
  • 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。

运行时多态有三个条件:

  • 继承
  • 覆盖(重写)
  • 向上转型

下面的代码中,乐器类(Instrument)有两个子类:Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。

案例一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Instrument {
public void play() {
System.out.println("Instument is playing...");
}
}

public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}

public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}

public class Music {

public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}
输出:
1
2
Wind is playing...
Percussion is playing...
案例二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class A {
public String show(D obj){
return ("A and D");
}

public String show(A obj){
return ("A and A");
}

  
public class B extends A{
public String show(B obj){
return ("B and B");
}

public String show(A obj){
return ("B and A");
}
}
  
public class C extends B{

}

  
public class D extends B{

}

public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();

System.out.println("1:"+a1.show(b));
System.out.println("2:"+a1.show(c));
System.out.println("3:"+a1.show(d));
System.out.println("4:"+a2.show(b));
System.out.println("5:"+a2.show(c));
System.out.println("6:"+a2.show(d));
System.out.println("7:"+b.show(b));
System.out.println("8:"+b.show(c));
System.out.println("9:"+b.show(d));
}
}
输出:
1
2
3
4
5
6
7
8
9
1:A and A
2:A and A
3:A and D
4:B and A
5:B and A
6:A and D
7:B and B
8:B and B
9:A and D
分析4:

a2.show(b),a2是一个引用变量,类型为A,则this为a2,b是B的一个实例,于是它到类A里面找show(B obj)方法,没有找到,于是到A的super(超类)找,而A没有超类,因此转到第三优先级this.show((super)O),this仍然是a2,这里O为B,(super)O即(super)B即A,因此它到类A里面找show(A obj)的方法,类A有这个方法,但是由于a2引用的是类B的一个对象,B覆盖了A的show(A obj)方法,因此最终锁定到类B的show(A obj),输出为”B and A”。

分析5:

a2.show(c),a2是A类型的引用变量,所以this就代表了A,a2.show(c),它在A类中找发现没有找到,于是到A的超类中找(super),由于A没有超类(Object除外),所以跳到第三级,也就是this.show((super)O),C的超类有B、A,所以(super)O为B、A,this同样是A,这里在A中找到了show(A obj),同时由于a2是B类的一个引用且B类重写了show(A obj),因此最终会调用子类B类的show(A obj)方法,结果也就是B and A。

分析8:

b.show(c),b是一个引用变量,类型为B,则this为b,c是C的一个实例,于是它到类B找show(C obj)方法,没有找到,转而到B的超类A里面找,A里面也没有,因此也转到第三优先级this.show((super)O),this为b,O为C,(super)O即(super)C即B,因此它到B里面找show(B obj)方法,找到了,由于b引用的是类B的一个对象,因此直接锁定到类B的show(B obj),输出为”B and B”。

总结:

当超类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。在继承链中对象方法的调用存在一个优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

7、this与super对比

  • this.属性 操作当前对象的属性。
  • this.方法 调用当前对象的方法 。
  • super相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员。
  • 子类中的成员变量或方法与父类中的成员变量或方法名同名时,表示调用父类的成员。
  • super引用构造函数:调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。默认在构造函数第一条语句是“super();”,无论写与否。
  • super(参数):调用超类(父类)中的某一个构造函数(应该为构造函数中的第一条语句)。
  • this(参数):调用本类中另一种构造函数(应该为构造函数中的第一条语句)。
  • 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
  • super() 和 this() 类似,区别是:super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。
  • super() 和 this() 均需放在构造方法内第一行。
  • this 和 super 不能同时出现在一个构造函数里面,因为 this 必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this() 和 super() 都指的是对象,所以均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。
  • 从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。

8、final

表示最终的意思,可以修饰类、成员变量、成员方法。

  • 修饰类:类不可以被继承。
  • 修饰成员变量:变量为常量,值不可以改变。
  • 修饰成员方法:方法不能被重写。
  • final还可以修饰局部变量
    • 修饰基本数据类型,值不能改变。
    • 修饰引用数据类型,地址值不能改变。

9、static

static表示静态的意思,既可以修饰成员变量,又可以修饰成员方法,还有一种特殊用法修饰类。

  1. 修饰成员变量表示静态变量:static 变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。static不能修饰局部变量
  2. 修饰成员方法:static 方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this 的,因为它不依附于任何对象,既然都没有对象,就谈不上this 了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。
  3. static代码块:static 关键字还有一个比较关键的作用就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static 块,并且只会执行一次。
static的特点:
  • 随着类的加载而加载。
  • 优先于对象存在。
  • 被所有的对象所共享。
static的注意事项:
  • 在静态方法中,不能出现this/super。
  • 静态方法只能访问静态成员。非静态方法既可以访问静态成员,也可以访问非静态成员。
  • 工具类里面的成员一般来说是静态成员(目的:节约内存空间)。

10、protected

private 访问修饰符,对于封装而言,是最好的选择,但这个只是基于理想的世界,有时候我们需要这样的需求:我们需要将某些事物尽可能地对这个世界隐藏,但是仍然允许子类的成员来访问它们。这个时候就需要使用到 protected 。

对于 protected 而言,它指明就类用户而言,他是 private ,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。

11、静态变量和成员变量的区别

  1. 所属不同
    • 静态变量属于类,也称为类变量。
    • 成员变量属于对象,也称为实例变量。
  2. 内存中位置不同
    • 静态变量存在方法区。
    • 成员变量存在堆中。
  3. 出现的时间不同
    • 静态变量随着类的加载而加载,随着类的消亡而消亡。
    • 成员变量随着对象的创建而创建,随着对象的消失而消失。
  4. 调用方式不同
    • 静态变量通过类名调用,也可以通过对象名调用(不建议)。
    • 成员变量只能通过对象名调用。

所以,成员变量可以称为对象的特有数据,静态变量称为对象的共享数据。

12、成员变量与局部变量的区别

  1. 在类中的位置不同
    • 成员变量:在类的方法外。
    • 局部变量:在方法或者代码块中,或者在方法的声明上(即在参数列表中)。
  2. 在内存中的位置不同
    • 成员变量:在堆中(方法区中的静态区)。
    • 局部变量:在栈中。
  3. 生命周期不同
    • 成员变量:随着对象的创建而存在,随着对象的消失而消失。
    • 局部变量:随着方法的调用或者代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失。
  4. 初始值
    • 成员变量:有默认初始值。
    • 局部变量:没有默认初始值,使用之前需要赋值,否则编译器会报错(The local variable xxx may not have been initialized)。

13、抽象类与抽象方法

在 Java 中,一个没有方法体的方法称为抽象方法。而一个类中如果有抽象方法,那么这个类就称之为抽象类。一般当我们设计一个类,不需要创建此类的实例时,可以考虑将该类设置成抽象类,让其子类实现这个类的抽象方法。

抽象类的特点:
  1. 抽象类不可以实例化(不能用new关键字创建抽象类实例)。
  2. 抽象类是有构造器的(所有类都有构造器)。
  3. 抽象类不一定有抽象方法,但是有抽象方法的类一定是抽象类。
  4. 抽象类的子类,可以是抽象类,也可以是具体类。如果子类是具体类,需要重写抽象类里面所有抽象方法
抽象方法的特点:
  1. 格式,没有方法体,包括{},例如:public abstract void drinking();
  2. 抽象方法只保留方法的功能,具体的执行,交给继承抽象类的子类,由子类重写抽象方法。
  3. 如果子类继承抽象类,并重写了父类的所有的抽象方法,则此子类不是抽象类,可以实例化。
  4. 如果子类继承抽象类,没有重写父类中所有的抽象方法,意味着子类中还有抽象方法,那么此子类必须必须声明为抽象的。
abstract 不能与那些关键字共存?
  • private:因为一个 abstract 方法需要被重写,所以不能修饰为 private 。
  • final:因为一个 abstract 方法需要被重写。被 final 修饰的方法是不能被重写的,所以不能同 final 共存。
  • static:因为一个 abstract 方法没有方法体。静态方法需要对方法体执行内容分配空间,所以不能同 static 共存。
  • synchronized:是同步的,然而同步需要具体的操作才能同步,但 abstract 只有声明没有实现,所以不能同步。
  • native:他们本身的定义就是冲突的, native 声明的方法是移交本地操作系统实现的,而 abstract 是移交子类对象实现的,同时修饰的话,导致不知道谁实现声明的方法。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class Person {
//抽象类的构造方法
public Person(){

}
//抽象方法,无{}方法体
public abstract void Drinking();
//非抽象方法
public void Eating(){
System.out.println("Eating");
}
}

public class Student extends Person{
@Override
public void Drinking() {
System.out.println("Drinking");
}
}

public class Test {
public static void main(String[] args) {
Student student = new Student();
student.Drinking();
student.Eating();
}
}
输出:
1
2
Drinking
Eating

14、接口

接口( Interface )在 Java 编程语言中是一个抽象类型,是抽象方法的集合,接口通常以 interface 来声明。实际上是一个规范,它会要求你做什么,但不会要求你去怎么做。接口里面定义的是额外功能,但是不给出具体的实现。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。

为什么要用接口?
  • 接口被用来实现解耦。

  • 接口被用来描述一种抽象。

  • Java 不像 C++ 一样支持多继承,所以 Java 可以通过实现接口来弥补这个局限。
接口与类相似点:
  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。
接口与类的区别:
  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。
接口特性:
  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
抽象类和接口的区别:
  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
接口和接口继承关系:
  • 单继承
  • 多继承
  • 多级继承
CATALOG
  1. 1. 有关面向对象的一些总结
    1. 1.0.1. 1、面向过程的思想和面向对象的思想
    2. 1.0.2. 2、面向对象的设计思想
    3. 1.0.3. 3、类和对象
    4. 1.0.4. 4、封装
    5. 1.0.5. 5、继承
      1. 1.0.5.0.1. 继承的优点:
      2. 1.0.5.0.2. 继承的特性:
  2. 1.0.6. 6、多态
    1. 1.0.6.0.1. 案例一:
    2. 1.0.6.0.2. 输出:
    3. 1.0.6.0.3. 案例二:
    4. 1.0.6.0.4. 输出:
    5. 1.0.6.0.5. 分析4:
    6. 1.0.6.0.6. 分析5:
    7. 1.0.6.0.7. 分析8:
    8. 1.0.6.0.8. 总结:
  • 1.0.7. 7、this与super对比
  • 1.0.8. 8、final
  • 1.0.9. 9、static
    1. 1.0.9.0.1. static的特点:
    2. 1.0.9.0.2. static的注意事项:
  • 1.0.10. 10、protected
  • 1.0.11. 11、静态变量和成员变量的区别
  • 1.0.12. 12、成员变量与局部变量的区别
  • 1.0.13. 13、抽象类与抽象方法
    1. 1.0.13.0.1. 抽象类的特点:
    2. 1.0.13.0.2. 抽象方法的特点:
    3. 1.0.13.0.3. abstract 不能与那些关键字共存?
    4. 1.0.13.0.4. 示例:
    5. 1.0.13.0.5. 输出:
  • 1.0.14. 14、接口
    1. 1.0.14.0.1. 为什么要用接口?
    2. 1.0.14.0.2. 接口与类相似点:
    3. 1.0.14.0.3. 接口与类的区别:
    4. 1.0.14.0.4. 接口特性:
    5. 1.0.14.0.5. 抽象类和接口的区别:
    6. 1.0.14.0.6. 接口和接口继承关系: