面向对象编程 (OOP )
文章目录
基础知识
类与对象
面向对象编程( Object Oriented Programming ),就是要把一个软件看成是由具有各种功能特定职责(即方法)的对象( Object )组成的,各个对象又有他们特定的属性,各个对象之间可以相互作用和相互通讯但是又相对独立,它们共同组成了一个软件,而这些对象中具有相同特性的就可以归为一个类(Class)。
比如我们要完成“打扫卫生”这项任务,我们需要“人”来打扫卫生,这个“人”就是一个类,是具有相同特征的对象的抽象合称。“人”里面就有张三、李四、王五、赵六,这些具体的“人”就是对象。每个“人”的“名字”就是他们特有的属性。然后我们给每个“人”分配任务,张三扫地,李四拖地,王五擦窗,赵六清理杂物,这就是他们每个人的方法,即特定的功能和职责。所有对象共同完成了打扫卫生这项任务。
Java 是典型的面向对象的语言,我们通过以下一段程序看如何定义类与对象:
class Person { // 类:创建“人”
String name; // 参数:人有姓名
public void say(){ // 方法:人会说话,方法名为say
System.out.println("Hello World");
}
public static void main(String[] args) { //主方法 main
Person He = new Person(); //对象:创建具体的“人”
He.name = "Tony"; //属性:对象的name为“Tony”
He.say(); //调用对象的say方法
}
}
面向过程与面向对象
二者区别简述
面向过程是具体化的,流程化的,解决一个问题, 分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了 。
面向对象是模型化、模块化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法,需要什么功能直接使用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们我们使用的就是面向对象了。
这里借用网络上的一个例子来说明两者区别:
在五子棋游戏的设计中,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。
如果是面向对象的设计思想来解决问题。那么整个五子棋可以分为:1、黑白双方,这两方的行为是一模一样的;2、棋盘系统,负责绘制画面;3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
优缺点总结
面向过程:
- 优点:性能比面向对象高,因为类调用时需要实例化(创建对象),开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展。
面向对象:
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护。
- 缺点:性能比面向过程低。
OPP基础
数据类型
Java中数据类型分为两类:基本数据类型和引用数据类型。
基本数据类型
基本数据类型有过一丁点编程基础的都应该有所了解了,不做过多描述。
类型名称 | 类型定义 | 取值范围 |
---|---|---|
boolean | 布尔值,作二元判断 | true, false |
byte | 8位有符号整数 | -128 ~ 127 |
short | 16位有符号整数 | -32768 ~ 32767 |
int | 32位有符号整数 | -2147483648(-231)~ 2147483647(231-1) |
long | 64位有符号整数 | -263~ (263-1) |
float | 32位浮点数 | 1.4E-45 ~ 3.4028235E38 |
double | 64位浮点数 | 4.9E-324 ~ 1.7976931348623157E308 |
char | 16位Unicode字符 | 0 ~ 65535 |
注意:
-
boolean不可以转换为其他的数据类型;
-
整数型,浮点类型,字符型是可以相互转换的,转换时遵守下面的原则:
-
自动转换:把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,系统将可以进行自动类型转换。
数据类型按容量大小排序为: byte, short, char < int < long <float < double
-
byte, short, char 三种类型间不会相互转换,他们三者在计算时,首先会转换为int类型;
-
强制转换:容量大的类型在转换为小的类型的时候,必须加上强制转换符,此时可能造成精度降低或者溢出问题。
例如将 double 型的 R 强制转换为 int 型:
(int)R
-
有多种数据类型混合计算的时候,系统首先自动转换为容量最大的那个类型再来继续计算;
-
默认的浮点数类型是double,如:1.2; 默认的整数类型是int类型,如:1。
引用数据类型
除了以上基本数据类型, 我们也可以自定义一种引用数据类型来描述一个事物,而不是Java为我们预先提供好的类型。Java 中引用类型有三种,分别是类类型(class)、接口类型(interface)、数组类型 。
值传递与引用传递
先介绍一下背景,Java中把内存分为两类:
- **栈(stack):**存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中。
- **堆(heap):**存放用new产生的数据。
所有的类型在内存中都会分配一定的存储空间, 基本数据类型只有一块存储空间(分配在stack中), 而引用类型有两块存储空间(一块在stack中,一块在heap中) 。
基本数据类型的数据传递为值传递,引用数据类型的数据传递为引用传递,二者的区别下面通过具体代码来讨论。
值传递
通过简单地交换两个 int 变量的值来讨论值传递:
class Change {
//交换两个变量的值
public static void Swap(int a,int b){
int c=a;
a=b;
b=c;
System.out.println("a = "+a);
System.out.println("b = "+b);
}
public static void main(String[] args){
int c=10;
int d=20;
Swap(c,d);
System.out.println("After Swap:");
System.out.println("c = "+c);
System.out.println("d = "+d);
}
}
输出结果为:
a = 20
b = 10
After Swap:
c = 10
d = 20
从以上代码可以看出:虽然在 Swap (a,b) 方法中改变了传进来的参数的值,但对这个参数源变量本身并没有影响,即对 main(String[]) 方法里的 a,b 变量没有影响。
那说明,参数类型是基本数据类型时,是按值传递的。以参数形式传递简单类型的变量时,实际上是将参数的值作了一个拷贝传进方法函数的,那么在方法函数里再怎么改变其值,其结果都是只改变了拷贝的值,而不是源值。
引用
引用就像是一个对象的名字或者别名, 一个对象在堆内存(heap)中会请求一块空间来保存数据,访问对象的时候,我们不会直接是访问对象在内存中的数据,而是通过引用去访问,这个引用也是一种数据类型,保存在栈内存(stack)中。相当于栈内存中保存了在堆内存中对象的名字,我能通过栈内存中的名字找到堆内存中的对象。
如果我们定义了不止一个引用指向同一个对象,这些引用是不相同的,但是它们的值是相同的,都指示同一个对象在堆内存(heap)的中位置,相当于一个对象的不同名字。比如:
String a="This is a Text!";
String b=a;
我们使用了两个定义语句来定义 a 和 b,他们是两个不同的引用,但它们的值是一样的,都指向同一个对象 “This is a Text!” 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H72ZxQlK-1573119674446)(C:\Users\67156\Desktop\img.png)]
要注意 String 对象的值本身是不可更改的 ,像 b = "World Tow"; b = a;
这种情况不是改变了 “World” 这一对象的值,而是改变了它的引用 b 的值使之指向了另一个 String 对象 a 。
如图,开始b 的值为绿线所指向的“Word Two”,然后 b = a; 使 b 指向了红线所指向的 “Word”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cklsGcFx-1573119674448)(C:\Users\67156\Desktop\img2.png)]
两个要点:
(1) 引用是一种数据类型(保存在stack中),保存了对象在堆内存(heap)中的地址,这种类型既不是我们平时所说的基本数据类型也不是类实例(对象);
(2) 不同的引用可能指向同一个对象,换句话说,一个对象可以有多个引用,即该类类型的变量。
引用传递
看下面一段代码:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
change(arr);
System.out.println(arr[0]);
}
//将数组的第一个元素变为0
public static void change(int[] array) {
int len = array.length;
array[0] = 0;
}
运行的结果是:
0
调用change()的时候,形参array接收的是arr地址值的副本。并在change方法中,通过地址值,对数组进行操作。change方法弹栈以后,数组中的值已经改变。main方法中,打印出来的arr[0]也就从原来的1变成了0。
无论是主函数,还是change方法,操作的都是同一个地址值对应的数组。
特殊的:String类型的传递
public static void main(String[] args) {
String str = "AAA";
change(str);
System.out.println(str);
}
public static void change(String s) {
s = "abc";
}
运行的结果是:
运行的结果是:
AAA
String 是一个类,类是引用数据类型,做为参数传递的时候,应该是引用传递。但是从结果看起来却是值传递。 原因是什么呢?
String 的 API 中有这么一句话:
“their values cannot be changed after they are created”
意思是:String的值在创建之后不能被更改。
API 中还有一段:
String str = "abc";
等效于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);
也就是说:对 String 对象 str 的任何修改等同于重新创建一个对象,并将新的地址值赋值给 str。
封装、继承、多态
封装
封装(Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
一般我们可以修改属性的可见性来限制对属性的访问(一般限制为 private) ,例如以下这段代码中,将 name 和 age 属性设置为私有的,只能本类才能访问,其他类都访问不了,如此就对信息进行了隐藏。
public class Student {
private String name;
}
然后,我们可以对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问 :
public class Student{
private String name;
public void setName(String name){
this.name = name;
}
public void say(){
System.out.println("My name is "+ name);
}
}
继承
继承是 java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承类
例如上面的代码中我们定义了 Student 类,学生中又会分为大学生等类,而大学生类中也会有和学生类相同的属性和方法,为了减少代码的冗余,也为了体现真实的类与类之间的关系,我们定义大学生类时就可以继承学生类的属性和方法:
public class unStudent extends Student{ //继承Student类的属性和方法
public static void main(String[] args){
unStudent uns = new unStudent;
uns.setName("Tony");
uns.say(); //调用父类继承来的方法
}
}
输出结果为:
My name is Tony
继承后的 unStudent 类叫 Student 类的子类,Student 类为 unStudent 类的父类。我们在 unStudent 类中调用的 say 方法实际上是从父类继承来的。
当然,我们也可以给子类定义特有的方法,但是一定要注意只有子类类型对象才能调用子类特有的方法。比如,大学生需要通过CET4:
public class unStudent extends Student{ //继承Student类的属性和方法
public void CET4(){
System.out.println("I need to pass CET4.");
}
public static void main(String[] args){
unStudent uns = new unStudent();
uns.setName("Tony");
uns.say(); //调用父类继承来的方法
uns.CET4(); //调用子类特有方法CET4
}
}
继承接口
在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。
使用 implements 关键字可以变相的使 java 具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
public interface A {
public void eat();
}
public interface B {
public void show();
}
//继承接口A和B
public class C implements A,B {
}
方法的重写
子类从父类继承来的方法不一定完全符合子类的具体行为,所以我们可以根据子类自身情况来重写继承来的方法体:
public class unStudent extends Student{ //继承Student类的属性和方法
public void say(String name){ //重写say方法
System.out.println("I am a college student,My name is "+ name);
}
public static void main(String[] args){
unStudent uns = new unStudent();
uns.setName("Tony");
uns.say("Tony"); //调用重写的方法
}
}
输出结果为:
I am a college student,My name is Tony
super 与 this
- super关键字:我们可以通过 super 关键字来实现对父类成员的访问,用来引用当前对象的父类。
- this关键字:指向自己的引用。
注意:
- 当子类和父类都有同名的属性时,在子类中如果要使用父类的属性 super . 属性。
- 只能应用在成员方法和构造方法中,不能用在静态方法中。
- 如果在构造方法中使用必须放在第一行。
- 在构造方法中 this() 和 super() 不能同时出现。
1、直接引用:
子类中的成员变量或方法与父类中的成员变量或方法同名时用 this 调用子类,super 调用父类。
public class unStudent extends Student{
public void say(){
System.out.println("I am a college student");
}
public void test(){
this.say(); //调用自己的方法
super.say(); //调用父类的方法
}
public static void main(String[] args){
unStudent uns = new unStudent();
uns.setName("Tony");
uns.test();
}
}
输出结果为:
I am a college student
My name is Tony
2、引用构造方法:
什么是构造方法呢?从字面上理解即为构建创造时用的方法,即就是对象创建时要执行的方法。既然是对象创建时要执行的方法,那么只要在new对象时,知道其执行的构造方法是什么,就可以在执行这个方法的时候给对象进行属性赋值。构造方法没有返回值类型,也不需要写返回值。因为它是为构建对象的,对象创建完,方法就执行结束。构造方法名称必须和类名保持一致。
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
父类代码:
public class Student{
//构造方法1
Student(){
System.out.println("父类,无构造方法:"+"My name");
}
//构造方法2
Student(String name){
System.out.println("父类,含一个参数的构造方法:"+"My name is "+name);
}
}
子类代码:
public class unStudent extends Student{
unStudent(){
super(); //调用父类构造方法1
System.out.println("子类,调用父类无参数构造方法:"+"My name");
}
unStudent(String name){
super(name); //调用父类具有相同形参的构造方法2
System.out.println("子类,调用父类有一个参数构造方法:"+"My name is "+name);
}
unStudent(String name, int age){
this(name);
System.out.println("子类,调用子类有相同形参构造方法:"+"My age is "+age);
}
public static void main(String[] args){
unStudent uns1 = new unStudent();
unStudent uns2 = new unStudent("Tony");
unStudent uns3 = new unStudent("Tony",20);
}
}
输出结果:
父类,无构造方法:My name
子类,调用父类无参数构造方法:My name
父类,含一个参数的构造方法:My name is Tony
子类,调用父类有一个参数构造方法:My name is Tony
父类,含一个参数的构造方法:My name is Tony
子类,调用父类有一个参数构造方法:My name is Tony
子类,调用子类有相同形参构造方法:My age is 20
多态
多态是同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作。
多态存在的三个必要条件:继承、重写、父类引用指向子类对象。比如:
Parent p = new Child();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
抽象类、接口
抽象类
抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法必须用 abstract 关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用 abstract 关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。抽象类的声明格式为:
public abstract class ClassName {
abstract void fun();
}
抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为 abstract 方法,此时这个类也就成为 abstract 类了。
包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。抽象类和普通类的主要有三点区别:
- 抽象方法必须为 public 或者 protected(因为如果为 private,则不能被子类继承,子类便无法实现该方法),默认情况下默认为 public。
- 抽象类不能用来创建对象;
- 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为 abstract 类。
接口
接口中可以含有变量和方法,是一种极度抽象的类型,它比抽象类更加"抽象",并且一般情况下不在接口中定义变量。 但是要注意,接口中的变量会被隐式地指定为 public static final 变量(并且只能是 public static final变量,用 private 修饰会报编译错误),而方法会被隐式地指定为 public abstract 方法且只能是 public abstract 方法(用其他关键字,比如 private、protected、static、 final 等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。 在 Java 中,定一个接口的形式如下:
public interface InterfaceName {
}
要让一个类遵循某组特地的接口需要使用 implements 关键字,一个类可以继承多个接口。具体格式如下:
class ClassName implements Interface1,Interface2,[....]{
}
如果一个非抽象类遵循了某个接口,就必须实现该接口中的所有方法。对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法。