第9章-封装
概述
- 在使用面向对象的思想进行抽象的过程,也就是按照业务特性定义类的过程,就是封装
- 封装主要目的是让类足够独立(内聚),只包含职责相关的内容,并需要隐藏类的细节,只暴露出想要展现的方法
- 生活场景
- 鼠标,这个天天都在用的电脑配件,主要只有左键、右键、滚轮三个与人交互的按钮;但其内部,实际是经过了很多代的迭代,从最早的有线、滚动使用小圆球方式,到无线,到滚动使用红外线,到无线电波。---这就是一个典型的封装,与人交互的永远不变,照顾使用习惯,但内部隐藏功能部分,则持续修改
- 汽车,对驾驶人操作提供支持的主要功能,从有以来,没有太大变化,离合、刹车、油门、档位等;但内部,涉及了成千上万的零件,在持续的迭代,像发动机,有1.5T、2.0T等不同容量或是电动车的电机,刹车制动方式有多种,或多种配合。---这也是典型的封装,把复杂的隐藏起来,上百年来提供统一的操作体验(即暴露稳定的接口)
- 在企业某个HR系统(或是学校的学生系统中),为了员工(或学生)之间能相互联系,会对其他同学展现学生姓名、性别、参加的社团等,但肯定不会展现工资、银行卡号、家庭住址等信息
- ...
- 对于这些生活场景的软件设计与开发过程中,具体到定义某个类,都不是随意的,里面的所有的属性和方法都必须是与这个类相关的;同时这些属性和方法,面向其他类是否能访问,能访问哪个,也是要仔细思考的
- 封装时要注意的一些原则和细节
- 业务相关性,比如,一般定义的老师类中有教龄属性、授课方法,但学生类就不会有这个教龄属性、授课方法
- 隐藏细节,不呈现一些私密属性,不展示一些特别的加工方法;比如,一个员工类,银行账户属性就不能向其他类随意访问;一个账户类,就不能把里面登录时加密的细节公开出去
- 暴露方法,类不是自闭的,必须与其他类协同才能工作,通过方法定义,结合访问修饰符,暴露需要的接口;比如,一个账户类,里面的加密方法不对外暴露,但登录方法则会对外暴露,作为类与外部类交互窗口
- 设置访问权限,暴露的接口中,有些可能希望A能访问,有些可能B才能访问,这也是实际的需求,多使用访问修饰符来处理
- 附加业务规则,一些属性的值其实是有限定业务规则的,比如学生的年龄属性,就不能小于0
好封装的优点
- 代码更合理、更内聚,所有的属性与方法,都与类直接相关,职责更清晰
- 安全、体验更好,隐藏不需要使用者关注的复杂细节
- 使用简单,只暴露必须且简单的接口,其他类使用更容易,交互更简便
- 代码可维护性好,只要暴露的接口不变,可以随时调整实现逻辑
- ...
访问修饰符
用于控制类、属性、方法、接口等的可访问性,实现封装的细节隐藏与接口暴露
主要包括public、protected、默认访问修饰符(不写)、private(对类和接口一般只有public、默认访问修饰符),具体规则如下:
√表示可以访问,否则不能访问
访问修饰符规则 应用实例
- 实例1,访问修饰符,进行代码演示;具体看“代码和工具”下本章的应用实例目录中的access-modifier项目
【练习】
- 练习实例内容,动手熟悉访问修饰符特点
属性封装
基于业务和封装的要求,属性不应该再被直接访问,而应该定义为private,使用方法进行访问
所谓属性访问,即设置属性值、获取属性值,也称为getter和setter
另外,根据实际的业务,可能存在只读属性、只写属性,如下面的性别昵称、密码属性
并且,在设置和获取时,可以施加一些业务逻辑限制
应用实例
应用实例1,属性封装,标准getter/setter
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类
*/
public class Employee {
//属性,一般都设置为private
//姓名
private String name;
//性别
private char sex;
//年龄
private int age;
//密码
private String password;
//血型
private String bloodType;
//getter/setter
//名称
public String getName() {
//姓名返回脱敏,substring取第1个字符
String desensitizationName = this.name.substring(0, 1) + "**";
return desensitizationName;
}
public void setName(String name) {
//添加限制
if (name == null) {
System.out.println("姓名不能为空!");
} else {
//设置值
this.name = name;
}
}
//性别
public char getSex() {
return sex;
}
public void setSex(char sex) {
//添加限制
if (sex != '男' && sex != '女') {
System.out.println("性别只能是[男]或[女]!");
} else {
this.sex = sex;
}
}
//年龄
public int getAge() {
return age;
}
public void setAge(int age) {
//添加限制
if (age < 0) {
System.out.println("年龄不能小于0岁!");
} else {
this.age = age;
}
}
//密码,【只写】
public void setPassword(String password) {
//添加限制
if (password == null) {
System.out.println("密码不能为空!");
} else {
if (password.length() < 6) {
System.out.println("密码长度不能小于6位!");
} else {
//设置值
this.password = password;
}
}
}
//血型,【只读】
public String getBloodType(){
return this.bloodType;
}
//构造方法
//无参构造
public Employee() {
}
//有参构造
public Employee(String name, char sex, int age,String password,String bloodType) {
this.name = name;
this.sex = sex;
this.age = age;
this.password = password;
this.bloodType = bloodType;
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//定义引用,并指向新建对象
Employee employee = new Employee("张三",'男',18,"123456","B型");
//设置属性
//姓名
employee.setName("尼古拉斯.赵四");
//性别
employee.setSex('a'); //设置失败
employee.setSex('女'); //设置成功
//年龄
employee.setAge(-30); //设置失败
employee.setAge(30); //设置成功
//密码,只写
employee.setPassword("123"); //设置失败,长度不够
employee.setPassword("123456"); //设置成功
//血型
// employee.setBloodType("A型"); //设置失败,没有此方法
//输出密码
//System.out.println(employee.getPassword()); //获取失败,没有定义该getter方法
//使用属性,输出
System.out.println("当前工作人员信息:[" + employee.getName() + "," + employee.getSex() + "," + employee.getAge() + "," + employee.getBloodType() + "]");
}
}输出结果
【练习】
- 练习实例内容,完成代码编写;掌握属性封装
方法封装
基于业务和封装的要求,不同的方法具备不同的访问特性
- 对外访问,用于被其他类调用,要保持稳定;一般使用public访问修饰符修饰;如应用实例中的login方法
- 对内访问,用于类内部、包内部调用,主要是根据业务特性、代码编写规划定义,相对灵活;一般使用private或默认修饰符修饰;如应用实例中的check方法
- 子类访问,用于被子类继承,体现父类模板特性,让所有子类具备此类访问;一般使用protected访问修饰符修饰
在封装的方法中,可根据业务,编写合理的代码,并调用其他类的方法协同工作
应用实例
应用实例1,方法封装,并换一个类中使用main尝试调用
实例代码
package com.bjpowernode.demo;
/**
* 用户登录类
*/
public class UserLogin {
/**
* 验证密码,封装原因:1、与User类相关;2、类内部使用,对内使用,相对灵活
* @param username 用户名
* @param password 密码
* @return 验证结果
*/
private boolean check(String username, String password){
//密码验证逻辑
if(username.equalsIgnoreCase("jack") && password.equalsIgnoreCase("123456")){
return true;
}else{
return false;
}
}
/**
* 登录,v1.0版本,封装特点:1、与User类相关;2、对外接口,需要保持稳定,方法签名一般不变
* @param username 用户名
* @param password 密码
* @return 是否登录成功
*/
public String login(String username,String password){
String result = "登录成功!";
//验证用户名密码
boolean checkResult = this.check(username,password);
if(checkResult == false){
result = "登录失败!";
}
return result;
}
/**
* 登录,v2.0版本,重载,封装特点:1、与User类相关;2、对外接口,需要保持稳定,方法签名一般来变
* @param username 用户名
* @param password 密码
* @return 是否登录成功
*/
public String login(String username,String password,boolean rememberMe){
String result = "登录成功!";
//验证用户名和密码
boolean checkResult = this.check(username,password);
if(checkResult == false){
result = "登录失败!";
}
//将rememberMe保存到缓存,调用其他类完成 todo
return result;
}
}
【练习】
- 练习实例内容,完成代码编写;掌握方法封装
this
概述
在对象内部存在,指代当前对象的一个关键字
定义于类(模板)中,但应用在实际的对象中
“ 一千个读者眼中就会有一千个哈姆雷特 ”,通过同一个类,创建的1000个对象,也各有特色,具备自己的this
this在JVM中的示意图,可以更准确的认识this
一般在普通方法和构造方法中使用
- 普通方法(实例方法)中,用于调用当前对象的其他属性或方法,直接通过“this.属性”、"this.方法名()"调用,简单、快捷
- 构造方法中,放置于第1行,用于调用当前对象的其他构造方法,直接通过“this(参数列表)”调用,减少多个构造方法中重复的初始化代码;注意,”this(参数列表)“必须在构造方法的第一行
不能在static修饰的静态方法中使用
应用实例
实例1,在普通方法中使用this
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类,在普通方法中使用this
*/
public class Employee {
//属性,一般都设置为private
//姓名
private String name;
//性别
private char sex;
//年龄
private int age;
//getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* 跟别人打招呼
*
* @param name 打招呼的人
*/
public void sayToSomeone(String name) {
//【在普通方法中使用this】this调用属性
System.out.println(this.name + "向" + name + "打招呼!");
}
/**
* 拍一拍
*
* @param name 拍一拍的人
*/
public void tapToSomeone(String name) {
System.out.println(this.name + "拍了拍" + name + "的肩膀!");
//【在普通方法中使用this】this调用方法
this.sayToSomeone(name);
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//定义引用,并指向新建对象
Employee employee = new Employee();
//设置属性
employee.setName("尼古拉斯.赵四");
//跟张三打招呼
employee.sayToSomeone("张三");
//拍了拍李四
employee.tapToSomeone("李四");
}
}
输出结果,运行代码查看
实例2,在构造方法中使用this
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类,在构造方法中使用this
*/
public class Employee {
//属性,一般都设置为private
//姓名
private String name;
//性别
private char sex;
//年龄
private int age;
/**
* 构造方法,带全部参数的初始化
*
* @param name
* @param sex
* @param age
*/
public Employee(String name, char sex, int age) {
//使用this访问属性
this.name = name;
this.sex = sex;
this.age = age;
}
/**
* 无参构造
*/
public Employee() {
//【在构造方法中使用this】使用另一个有参构造完成,必须放在第1行
this("匿名", '男', 0);
}
//getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//定义引用,并指向新建对象
Employee employee = new Employee();
//输出工作人员信息
System.out.println("当前工作人员:[" + employee.getName() + "," + employee.getSex() + "," + employee.getAge() + "]");
}
}输出结果,运行代码查看
实例3,验证this指向的是不同的对象
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类
*/
public class Employee {
/**
* 输出自己的this
*/
public void printSelfThis(){
System.out.println(this);
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//定义引用,并指向新建对象
Employee employee1 = new Employee();
//定义引用,并指向新建对象
Employee employee2 = new Employee();
//定义引用,并指向新建对象
Employee employee3 = new Employee();
//分别输出
System.out.println("--------employee1--------");
employee1.printSelfThis();
System.out.println(employee1);
System.out.println(employee1.toString());
System.out.println("--------employee2--------");
employee2.printSelfThis();
System.out.println(employee2);
System.out.println(employee2.toString());
System.out.println("--------employee3--------");
employee3.printSelfThis();
System.out.println(employee3);
System.out.println(employee3.toString());
}
}输出结果
结果解析
- 从上图可见,每个printSelfThis方法输出的this、使用时直接输出对象、使用时输出对象.toString()方法,其实输出的结果都是当前对象类型+序号,其中的序号也称为hashCode,每个对象唯一,使用16进制表示;这个内容,其实是对象的唯一标识
- 从上图可知,3个对象中的this,指向并不相同
【练习】
提问:
下面的代码是否有问题?为什么?
package com.bjpowernode.demo;
/**
* 工作人员类
*/
public class Employee {
//姓名
private String name;
/**
* 打招呼
*/
public static void sayHello(){
//使用this
System.out.println(this.name);
}
}下面的代码中,打了?号注释的代码,为什么要用this.name?与后面的name有啥区别?
package com.bjpowernode.demo;
/**
* 工作人员类
*/
public class Employee {
//姓名
private String name;
public void setName(String name) {
//?
this.name = name;
}
}
练习实例内容,完成代码编写;掌握this关键字的使用
static
概述
- 表示静态的,不依赖于对象存在,依赖类存在
- 能被所有类的对象共用,一个类有且只有一份,所有对象共用
- 常用来修饰属性、方法、代码块
- 使用static关键字
静态属性
定义方式与成员变量(属性)类似,访问修饰符后加static关键字,则就变成了类的静态属性
静态属性的getter/setter方法,也必须是静态的
应用实例
应用实例1,静态属性,员工记数
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类,员工记数
*/
public class Employee {
//静态属性,工作人员数量,并设置初始值
public static int employeeCount = 0;
//普通属性
//姓名
private String name;
/**
* 构造方法
*
* @param name 姓名
*/
public Employee(String name) {
this.name = name;
//每生成一个工作人员对象,数量加1
Employee.employeeCount++;
}
//getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//输出初始工作人员数量
System.out.println("初始工作人员数量:" + Employee.employeeCount);
//创建对象
new Employee("张三");
Employee e1 = new Employee("李四");
new Employee("");
new Employee("赵四");
//循环创建
for (int i = 0; i < 5; i++) {
new Employee(i + "");
}
//输出现在工作人员数量
System.out.println("此时工作人员数量:" + Employee.employeeCount);
}
}输出结果,运行程序查看
静态方法
在一些实际应用中,有些方法可能从业务上来说就不依赖类的对象而存在;也有些工具类,不需要生成对象
此时,这些方法就可以定义成静态方法
应用实例
应用实例1,静态方法,批量生成虚拟员工
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类,静态方法,批量创建虚拟工作人员
*/
public class Employee {
//成员变量
//姓名
private String name;
/**
* 生成虚拟工作人员,就不依赖某个对象而产生,而是由类进行,所以是静态的
*
* @param count
* @return
*/
public static Employee[] generateVitualEmployee(int count) {
//定义数组
Employee[] employees = new Employee[count];
for (int i = 0; i < count; i++) {
//创建对象
Employee employee = new Employee();
//设置属性
employee.name = "虚拟员工" + i;
//添加到数组
employees[i] = employee;
}
return employees;
}
//getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//创建20个虚拟工作人员
Employee[] employees = Employee.generateVitualEmployee(20);
//输出每个工作人员姓名
for (int i = 0; i < employees.length; i++) {
System.out.println("当前工作人员是:" + employees[i].name);
}
}
}输出结果,运行程序查看
应用实例2,静态工具类;员工信息检查工具类
实例代码
员工验证工具类,注意其中的静态验证方法
package com.bjpowernode.demo;
/**
* 员工信息验证工具
*/
public class EmployeeCheckUtil {
/**
* 验证密码
* @param password 密码
* @return 验证结果
*/
public static boolean checkPassword(String password) {
//验证结果,默认为验证通过
boolean result = true;
//验证
if (password == null || password.length() < 6) {
result = false;
}
return result;
}
/**
* 验证性别
* @param sex 性别
* @return 验证结果
*/
public static boolean checkSex(char sex){
//验证结果,默认为验证通过
boolean result = true;
//验证
if (sex != '男' && sex != '女') {
result = false;
}
return result;
}
}员工类,注意其中的性别设置、密码设置方法,使用了员工验证工具类的静态验证方法
package com.bjpowernode.demo;
/**
* 工作人员类,使用了员工信息验证工具类
*/
public class Employee {
//属性,一般都设置为private
//姓名
private String name;
//性别
private char sex;
//年龄
private int age;
//密码
private String password;
//getter/setter
//名称
public String getName() {
//姓名返回脱敏,substring取第1个字符
String desensitizationName = this.name.substring(0, 1) + "**";
return desensitizationName;
}
public void setName(String name) {
//添加限制
if (name == null) {
System.out.println("姓名不能为空!");
} else {
//设置值
this.name = name;
}
}
//性别
public char getSex() {
return sex;
}
public void setSex(char sex) {
//添加验证,使用工具类
if(EmployeeCheckUtil.checkSex(sex)){
this.sex = sex;
}else{
System.out.println("性别只能是[男]或[女]!");
}
}
//性别昵称,只读
public String getSexNick() {
String sexNick = "";
switch (this.sex) {
case '男':
sexNick = "伢子";
break;
case '女':
sexNick = "妹陀";
break;
}
return sexNick;
}
//年龄
public int getAge() {
return age;
}
public void setAge(int age) {
//添加限制
if (age < 0) {
System.out.println("年龄不能小于0岁!");
} else {
this.age = age;
}
}
//密码,只写
public void setPassword(String password) {
//添加验证,使用工具类
if(EmployeeCheckUtil.checkPassword(password)){
//设置值
this.password = password;
}else {
System.out.println("密码不能为空,且长度不能小于6位");
}
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//定义引用,并指向新建对象
Employee employee = new Employee();
//设置属性
//姓名
employee.setName("尼古拉斯.赵四");
//性别
employee.setSex('a'); //设置失败
employee.setSex('女'); //设置成功
//年龄
employee.setAge(-30); //设置失败
employee.setAge(30); //设置成功
//密码,只写
employee.setPassword("123"); //设置失败,长度不够
employee.setPassword("123456"); //设置成功
//输出密码
//System.out.println(employee.getPassword()); //获取失败,没有定义该getter方法
//使用属性,输出
System.out.println("当前工作人员信息:[" + employee.getName() + "," + employee.getSex() + "(" + employee.getSexNick() + ")," + employee.getAge() + "]");
}
}
静态代码块【扩展】
多用于在第一个对象创建前,初始化一些资源
定义在类内部
应用相对较少
应用实例
应用实例1,静态代码块,提前初始化
实例代码
package com.bjpowernode.demo;
/**
* 工作人员类,静态方法,批量创建虚拟工作人员
*/
public class Employee {
//静态代码块1
static {
System.out.println("静态代码块1");
}
//静态代码块2
static {
System.out.println("静态代码块2");
}
//成员变量
//姓名
private String name;
//getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 无参构造函数
*/
public Employee(){
System.out.println("构造函数");
}
/**
* 入口方法
*
* @param args
*/
public static void main(String[] args) {
//创建多个对象查看效果
new Employee();
Employee employee = new Employee();
new Employee();
}
}输出结果,运行程序查看
【练习】
提问:下面的代码正确吗?为什么?
package com.bjpowernode.demo;
public class Demo {
public static void main(String[] args) {
System.out.println("敲代码真快乐。");
static{
System.out.println("敲代码真快乐。");
}
}
}package com.bjpowernode.demo;
public class Demo {
//年龄
int age;
public static void main(String[] args) {
System.out.println(age);
}
}package com.bjpowernode.demo;
public class Demo {
//年龄
int age;
//静态代码块
static {
age = 18;
}
}
练习实例内容,完成代码编写;掌握static静态属性、static静态方法的使用,了解static静态代码块的使用和特性
实战和作业
编写程序,封装一个Book类,要求如下
- 具有属性:书名(title)、页数(pageNumber)
- 提供属性的getter/setter
- 页数不能少于200页,如果设置为少于200页时,提示页数不够
- 具备方法detail,输出书名和页数
再编写一个BookTest类,测试Book类的逻辑
编写程序,封装一个Account类,要求如下
- 具有属性:账号(id)、姓名(name)、余额(balance)
- 提供属性getter/setter
- 具备方法:
- 存款方法deposit,要求存款金额不能小于0
- 取款方法withdraw,要求取款金额不能超过余额
编写AccountTest类,测试Account类的逻辑
【扩展】重构上一章的Employee类和School类,具体要求
- 新加CommonUtil工具类,其中所有方法都是静态方法,包括
- 脱敏方法,类似于微信支付转账时显示的用户名效果,接收需要脱敏的原始字符串和替代字符两个参数,返回脱敏后的字符串;如果原始字符串是2个字符,只显示最后一个字符,如果超过2个字符,则只显示第1个和最后1个字符;如:方法接收参数是"张三"和'*',则返回"*三";方法接收参数为"尼古拉斯.赵四"和'-',则返回"尼-四"
- 整数范围限制验证方法,检查输入的整数是否在合理范围之内,接收需要验证的整数、最小值和最大值;如:方法接收15、1、10,则返回false;方法接收参数为5,2,8,则返回true
- Employee类,重构成getter/setter访问,并对属性进行限制
- 姓名,进行脱敏,使用CommonUtil工具类的脱敏方法
- 性别,做合适的处理
- 年龄,做合适的处理
- School类,重构成getter/setter访问,并对属性进行限制
- 类型,必须是使用的1~10之间,使用CommonUtil工具类的整数范围验证方法
- 员工列表,不能为null且员工数量必须大于等于1
- 新加CommonUtil工具类,其中所有方法都是静态方法,包括
【扩展】编写程序,封装Customer类、Employee类和EmployeeTest类,要求如下
- Customer类,客户类
- 具有属性:ID、姓名、年龄
- 提供属性的getter/setter
- 具备方法toString,返回所有属性信息
- Employee类,员工类
- 具备属性:ID、姓名、职级、是否为领导、领导(Employe类型)、客户列表(Customer[]类型)
- 提供属性的getter/setter
- 具备方法
- toString,返回ID、姓名、职级、领导姓名
- showCustomers,输出所有客户列表信息
- EmployeeTest类
- 定义一个管理者员工(是否为领导值为真)
- 设置id、姓名、职级为1、”张老大“、"L9"
- 定义一个普通员工(是否为领导值为假)
- 设置id、姓名、职级为7、”陈小云“、"L3"
- 设置他的领导为“张老大”
- 定义5个客户类对象,并分配给他
- 输出如下内容
- 普通员工“陈小云”的信息
- 普通员工“陈小云”的领导的信息
- 普通员工“陈小云”的所有客户信息
- 定义一个管理者员工(是否为领导值为真)
- Customer类,客户类