1. 面向对象程序设计概述
1.1 OOP
- OOP:Object Oriented Programming,面向对象编程;
- OOD:Object Oriented Design,面向对象设计;
- OOA:Object Oriented Analyse,面向对象分析。
面向对象程序设计(OOP)是当今主流的程序设计泛型。Java是完全面向对象的一门编程语言,它将一切以对象为载体去进行结构化设计和编程。
在以前的编程,是面向过程的,比如实现一个算术运算1+1=2,这种通过一个简单的语句就能实现。但随着业务和计算机科学的发展,我们需要编写越来越多的语句来实现某一个算法或需求,为了实现复用,我们将这些语句块抽象为一个个函数,也就是面向函数编程,通过组合不同函数,来实现某一个具体的业务需求或算法。再后来,我们发现某些函数具有一些相似点,比如我们现在有以下函数:
函数 | 用途 |
---|---|
before(Date time1, Date time2) | 判断time1时间是否在time2时间之前 |
after(Date time1, Date time2) | 判断time1时间是否在time2之后 |
int multi(int a, int b) | 获取a和b相乘结果 |
int add(int a, int b) | 获取a和b相加结果 |
int square(int a) | 获取a的平方 |
通过归类,我们发现,before和after函数,是和时间相关的,而multi、add、square是和数学计算相关的,此时,我们可以将这些函数,抽象到不同的类中,这就是面向对象编程。也就是说,在面向对象编程中,所有的算法、业务都是对象(或不同对象之间的交互),通过将现实的事物进行抽象,将现实生活的事物以及他们的关系,抽象到类中,从而实现对现实世界的抽象和建模。
public class TimeUtil {public boolean before(Date time1, Date time2) {...}public boolean after(Date time1, Date time2) {...}
}public class MathUtil {public int add(int a, int b) {return a + b;}public int multi(int a, int b) {return a * b;}public int square(int a) {return a * a;}
}
1.2 面向过程和面向对象区别
优点 | 缺点 | |
---|---|---|
面向过程 | 性能好,因为类调用需要实例化,开销较大,比较消耗资源 | 不易维护、不易复用、不易扩展 |
面向对象 | 易维护、复用、扩展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活、更易维护 | 性能比面向过程差 |
1.3 面向对象三大特性五大原则
1.4 类
类(class)是构造对象的模板或蓝图。由类构造(construct)对象的过程成为创建类的实例(instance),在Java中,所有编写的代码都位于某个类的内部。
封装(encapsulation)是与对象有关的一个重要概念,从形式上看,封装不过是将数据和行为组合在一个包汇总,并对对象的使用者隐藏了数据的实现方式。对象中的数据成为实例域(instance field),操作数据的过程成为方法(method),对于每一个特定的类实例(对象)都有一组特定的实例域值,这些值的集合就是这个对象当前的状态。
public class Person {// 实例域,也叫属性private int name;private int age;// 构造函数public Person(String name, int age) {this.name = name;this.age = age;}// 方法public void setName(String name) {this.name = name;}// 方法public String getName() {return this.name;}// 方法public void setAge(int age) {this.age = age;}// 方法public int getAge() {return this.age;}public static void main(String[] args) {// 实例化一个Person对象Person p1 = new Persion("cxy", 22);}
}
1.5 类之间的关系
在类之间,最常见的关系有:
- 依赖(uses-a):
- 聚合(has-a)
- 继承(is-a)
1.5.1 依赖(dependence)
依赖是一种最明显、最常见的关系,比如我们现在有两个类——Order订单类和Account账号类,Order类使用Account是因为Order对象需要访问Account对象来判断该账号的信用状态,因此Order类依赖Account类。也就是说,如果一个类的方法操纵了另一个类的对象,那么我们说一个类依赖于另一个类。
public class Account {private boolean status; // 信用状态...
}
public class Order {public boolean pay(Account account) {// 检测信用状态if (account.isCheckPass()) {// 支付} else {...}}
}
1.5.2 聚合(aggregation)
聚合关系因为这类A的对象包含类B的对象,比如对于一个用户,这个用户会有相关的权限信息,此时用户类包含权限类,因此他们之间是一个聚合的关系。
// 权限类
public class Permission {...
}public class User {private String username;// 一个用户有多个权限private Permission[] permissionArr;
}
1.5.3 继承(inheritance)
继承是一种用于表示特殊与一般关系的。继承在我们的现实生活中,是随处可见的,比如鸟是一种动物,那么它就具有动物所拥有的一切特性,而在这些特性的基础上,它又有自己的特性,因此,动物是父类,鸟是子类。父类和子类,反映的就是现实社会中矛盾的同一性和特殊性问题。我们通过抽象,将动物中的同一特性抽象到动物类中,而鸟类是在动物类的基础上(拥有动物类的所有特性)进行其自己的扩展实现。
public class Animal {
}
public class Bird extends Animal {// 鸟类的扩展,鸟类可以飞翔,因此有fly方法,而动物不一定都能飞,因此动物类没有fly方法public void fly() {}
}
2. 使用类
2.1 构造函数
当我们定义了某些类之后,如果要实例化一个类,需要使用new关键字来进行实例化。在每一个类中,都会有特殊的函数——构造函数(构造器),构造器和其他的方法有一个重要的不同,构造器总是伴随着new操作符的执行而被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。构造器具有以下要求:
- 构造器与类同名
- 每个类可以有1个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
如果类在定义时,没有实现构造函数,那么会使用java提供的默认构造函数。在下面的类中,没有自定义一个构造函数,那么java会自动认为该类具有以下构造函数:
// 默认构造函数
public Persion() {
}
我们可以自定义相关的默认函数,来限制类在初始化的时候,需要做什么事情,比如我们在Person类中加上一个构造函数,此时刚才的实例方式会报错。
因为在我们自定义的构造函数中,要求传入两个参数,所以,此时正常的实例化方式应该如下:
// 需要传入名字和年龄参数
Persion p1 = new Person("name", 12);
当然一个类可以有多个构造函数,从而提供不同的实例化方法,比如我们的person类,提供了三个构造函数,此时可以有三种实例化Person类的方法。
2.2 set方法和get方法
在Person类中,我们有两个属性——name属性和age属性,因为我们设置它们的修饰符为private,所以外部不能直接访问到这两个属性:
为了让外部类能访问到Person类的相关属性,我们一般都会给相关属性添加上get方法和set方法:
此时,外部类可以通过调用这些getter方法和setter方法来操作person对象的内部属性。
public class FirstSample {public static void main(String[] args) {Person p1 = new Person();System.out.println(p1.getName());System.out.println(p1.age);}
}
注意,get方法和set方法在概念上是有区别的,get方法仅仅查看并返回对象的状态,而set方法则是修改对象的状态。
2.3 访问控制符
在刚才的Person类中,Person类的name属性和age属性,是private的,而get方法和set方法,又是public的,这里涉及到private和public 这些访问控制符的定义。在Java中,有以下访问控制符:private、protected、public和default。
- public:具有最大的访问权限,可以访问任何一个classpath下的类、接口等,它往往用于对外的情况,也就是对象或类对外的一种接口的形式。
- protected:主要的作用的用来保护子类的,如果某个类定义它的属性为protected,那么它的子类可以直接访问这些protected的属性,它相当于传递给子类的一种继承的东西。
- default:它是针对本包访问而涉及的,任何处于本包下的类、接口都可以互相访问,即使是父类没有用protected修饰的成员也可以。
- private:访问权限仅限于类的内部,是一种封装的体现。大多数成员变量都是private的,它们不希望被其他任何外部类访问。
public:可以被所有其他类所访问
private:只能被自己访问和修改
protected:自身、子类及同一个包中类可以访问
default:同一包中的类可以访问,声明时没有加修饰符,认为是friendly。
3. 静态域和静态方法
3.1 静态域
如果将域定义为static,每个类中只有一个这样的域,而每个对象对于所有实例域都有自己的一份拷贝。也就是说,这个类的多个对象,会共享一个实例域。以Person类为例,假设我们的Person类定义如下,这个时候,所有的person对象都会共享同一个nextId,即使我们没有实例化一个Person对象,这个nextId也存在,因为它属于Person类,而不属于任何一个Person对象。
public class Person {private static int nextId = 0;private int id;private String name;private int age;...
}
通过这个nextId静态域,我们可以为每一个person对象分配唯一的id,如下图所示:
3.2 静态常量
静态变量我们使用比较少,但是静态常量使用比较多,例如,我们在Math类中定义一个静态常量:
public class Math {public static final double PI = 3.1415926;
}
在程序中,我们可以采用Math.PI的形式获取这个常量。
3.3 静态方法
静态方法是一种不能向对象实施操作的方法,例如Math类的pow方法就是一种静态方法, 它用于计算x的a次方。
Math.pow(x, a);
在上述的式子中,没有用到任何Math对象。因为静态方法不能操作对象,所以不能在静态方法中访问实例域,但是可以访问自身类中的静态域。此外对象可以调用当前类的静态方法,但是一般不推荐这样做,因为静态类的方法和当前对象其实并没有任何关系。
静态方法,一般在下列情况中使用:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供
- 一个方法只需要访问类的静态域
4. 方法参数
按值调用(call by value)表示方法接收的是调用者提供的值。按引用调用(call by reference)表示方法接收的是调用者提供的变量地址)。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
不同与C++可以决定方法参数是按值调用还是按引用调用,Java的方法都是采用按值调用的,也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
4.1 方法参数为基本数据类型
在上述例子中,x的值为10,然后threeMultiX会将入参的值乘以3,但是执行完threeMultiX后,x的值不变,仍旧是10,其本质原因就是因为java的参数为值拷贝,我们将上述的执行流程进一步细化如下:
1)初始化变量x,赋值为10
2)创建另一个变量x1,拷贝变量x的值给x1
3)进入threeMultiX方法,此时入参为x1,x1乘以3,此时x1的值为30
4)threeMultiX方法执行结束,打印x的值
4.2 方法参数为对象引用
如果方法参数为基本类型,我们可以看出,基本类型的值并不会发生改变,但如果方法参数为对象引用,就不同了。
上述结果的原因在于:
1)创建Person对象,此时p1变量引用该对象
2)p被初始化为p1值的拷贝,这里是一个对象的引用,也就是说,p和p1引用同一个对象
3)此时threeMultiAge方法,将p的age属性乘以3
4)threeMultiAge方法结束后,参数变量p不再使用,但p1变量仍然引用刚才的对象,而这个引用的对象,在threeMultiAge方法后,相关的age属性发生了变化,所以此时p1.getAge()的结果为66。
综上所述,如果方法参数是对象引用,那么在执行该方法的时候,入参是对象引用的拷贝,也就是所对象引用及其他拷贝同时引用一个对象,因此对这个对象的修改会被保留下来。
4.3 总结
- 一个方法不能修改一个基本数据类型的参数
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
5. 对象构造
5.1 重载
在之前的Person类中,有多个构造器:
public Person() {}public Person(String name) {this.name = name;}public Person(String name, int age) {this.name = name;this.age = age;this.id = buildId();}
这种特征叫做重载(overloading),如果多个方法有相同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出对应的方法,如果编译器找不到匹配的参数或者找出多个可能的匹配,就会产生编译时错误,这个过程被成为重载解析(overloading resolution)
5.2 默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。
5.3 无参数的构造器
如果咋i编写一个类时没有编写构造器,那么系统会提供一个无参数构造器,这个构造器将所有的实例域设置为默认值。
5.4 调用另一个构造器
关键字this引用方法的隐式参数,但这个关键字还有另外一个含义,如果构造器的第一个语句形如this(…), 这个构造器将调用同一个类的另一个构造器,如下代码所示:
public Person(String name) {this.name = name;}public Person(String name, int age) {this(name);this.age = age;this.id = buildId();}
6. 初始化块和静态初始化块
我们在初始化数据域时,一般是在数据域的声明中设置值,或者在构造器中设置值,但是java还有第三种机制:初始化块(initialization block)。在一个类的声明中,可以包含多个代码块,只要构造类的对象,这些块就会被执行。在执行顺序中,会先执行这些初始化块,然后才会执行构造器。但一般不推荐这样写,初始化的语句,一般直接放在构造器中就可以。
此外,还有静态初始化块,静态初始化块一个类只会执行一次。不同于初始化块在实例化对象时都会执行,静态初始化块在加载这个类的时候就会执行。在静态初始化块中,只能操作静态域,不能操作实例域,因为此时还没有任何实例。
7. 类设计技巧
- 一定要保证数据私有:不要破坏封装性。有时候需要编写一个get方法或set方法,但最好还是保持实例域的私有性。
- 一定要对数据初始化:java不对局部变量进行初始化,但会对对象的实例域进行初始化,最好不要依赖系统的默认值,而是应该显式地初始化所有的数据。
- 不要在类中使用过多的基本类型:用其他类代替多个相关的基本类型的使用。
- 不是所有的域都需要独立的域访问器和域更改器(get和set方法)
- 将职责过多的类进行分解。
- 类名和方法名要能体现它们的职责。
参考文档
https://blog.csdn.net/ThinkWon/article/details/100667386
https://www.cnblogs.com/jingmengxintang/p/5898900.html
《Java核心技术卷I》(网盘链接:https://pan.quark.cn/s/06c58d47dce1)