ZB-011-java封装和访问控制

什么是封装

  • 隐藏内部细节,只暴露出接口
  • 电灯
    • 你只关心它的“开关”接口,不关心内部的“电路”细节
  • 汽车
    • 你只关心“方向盘”,不关心内部的细节

Light.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Light{

public void trunOn(){
打开电路1();
打开电路2();
打开电路3();
}

public void 打开电路1(){}
public void 打开电路2(){}
public void 打开电路3(){}
}

Home.java

打开灯有两种方式

  • 直接调用 trunOn() (低耦合)
  • 调用实现细节 打开电路1();打开电路2();打开电路3()(高耦合)
1
2
3
4
5
6
7
8
9
10
11
class public Home{
public static void main(String[] args){
Light a = new Light();
a.trunOn();

Light b = new Light();
b.打开电路1();
b.打开电路2();
b.打开电路3();
}
}

如果有一天,一个高级工程师对打开灯的方式进行了优化

此时只要打开电路1();打开电路2(); 就可以开灯了

此时 以第二种方式调用开灯的人就要修改 因为它耦合了开灯的细节

一方改变另一方也要改变

而第一种方式只需要更改 turnOn() 自己一个方法就做到了正常工作

1
2
3
4
public void trunOn(){
打开电路1();
打开电路2();
}

场景二

Person类

1
2
3
4
5
public class Person{
int id;
int age;
String name;
}

假设10个人都用了你的 Person

1
2
3
Person p = new Person();
p.age = 10;
p.name = "张三";

老板突然提了个需求,如果年龄小与0则为0,大于100则为100

  • 这样别人都是通过 p.age = 10设置一个值。这样要改 10个地方。

优化

  • 把成员变量变为私有
  • 设置对应的 get/set 接口
  • 外界只能通过 get/set接口对成员进行操作
    1
    2
    3
    Person p = new Person();
    p.setAge(10);
    p.setName("张三");
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
public class Person {
private Integer id;
private String name;
// 如果年龄小与0则为0,大于100则为100
private int age;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
if(age<0){
this.age = 0;
}else if(age>100){
this.age = 100;
}else{
this.age = age;
}
}
}

有没有发现改一下接口 setAge() 就轻松的完成了需求

场景三,你开发的Person 被广泛应用到别人的电脑

此时你老板认为世界上只有男和女,于是你的gender 采用了 boolean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {

private String name;
private boolean gender;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
public boolean isMale() {
return gender;
}

public void setGender(boolean gender) {
this.gender = gender;
}
}

过了1年后,你老板发现还真有其他性别。你就不得不改变gender 为 String

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
package hello.service;

import java.util.Objects;

public class Person {

private String name;
private String gender;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
public boolean isMale() {
// return gender == "M"; 可能会空指针
// return "M".equals(gender); 非空对象前置
return Objects.equals(gender,"M");
}
// 废弃的注解
@Deprecated
public void setGender(boolean gender) {
this.gender = gender ? "M" : "F" ;
}
}

此时使用者无需任何改变,依然能正常使用

封装的实现

包的功能就是提供访问控制,一种边界,封装的边界

  • public 任何人都能访问
  • protected 只有子类和同包的可以访问
  • package private(包级私有)包权限 同包可访问
  • private 只有自己能访问

包是没有嵌套包含关系的!!!跟文件夹父子目录不一样

JavaBean约定

  • getter
  • setter
1
2
3
4
public class Person {
private String name;
private boolean cute;
}

此时 Person 的 name / cute 无法被外界访问因为是 private

设置getter/setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person {

private String name;
private boolean cute;

public String getName() {
return name;
}

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

public boolean isCute() {
return cute;
}

public void setCute(boolean cute) {
this.cute = cute;
}
}

为什么 getter/setter长这个样子,不是无缘无故,而是JavaBean约定

  • 我们知道 java 代表咖啡,而程序员很浪漫
  • java中创建对象了叫什么呢? 对象?太土了,程序员的浪漫促使它起了名字叫做 “Bean” 就是”豆”
  • JavaBean 就是咖啡豆

对于一个 JavaBean 来说 加入他有一个 getX() 和 setX() 方法,我们就认为它有一个 x 属性

规则如下
1
2
3
4
5
6
7
// 非 boolean 值
String name;
setName/getName

// boolean 值
boolean gender
setGender/isGender
这些约定有什么用呢?
  • 最重要之一就是 JSON 对象和字符串相互转换
1
2
3
4
5
6
// 如 js
var obj = {"name":"张三","age":"李四"};
// 序列化
JSON.stringify(obj);
// 反序列化
var obj2 = JSON.parse(`{"name":"张三","age":"李四"}`);

java常见序列化库 fastjson / gson / jackson

java中使用序列化库

maven里引入依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
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
public class Cat {

private String name;
private boolean cute;

public Cat(String name, boolean cute) {
this.name = name;
this.cute = cute;
}

public String getName() {
return name;
}

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

public boolean isCute() {
return cute;
}

public void setCute(boolean cute) {
this.cute = cute;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.json;
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {

Cat cat = new Cat("a",true);

System.out.printf(JSON.toJSONString(cat));

String s = "{\"cute\":true,\"name\":\"喵\"}";

cat = JSON.parseObject(s,Cat.class);
}
}

以上就是 JSON 的序列化和反序列化过程

JavaBean 约定

会使用你的 getter/setter 当作属性的名字,而不是你的成员private类型成员

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
package com.json;

public class Cat {
private boolean cute;

public Cat(String name, boolean cute) {
this.cute = cute;
}

public String getName() {
return "123";
}

public void setName(String name) {
}

public boolean isCute() {
return cute;
}

public void setCute(boolean cute) {
this.cute = cute;
}
}

// name属性不存在了
// getter/setter还在

public class Main {
public static void main(String[] args) {

Cat cat = new Cat("a",true);
System.out.printf(JSON.toJSONString(cat));
//{"cute":true,"name":"123"}
}
}

JavaBean总结

在Java的世界中,对json进行读写的时候,我们只看JavaBean的 getter/setter方法,而不看是否具有xxx属性

这也进一步验证了我们想要达到封装的目的,封装应该尽可能的隐藏内部实现细节,而仅仅像外界暴露接口

暴露的接口 在JavaBean中就是 getter/setter 方法

虽然每次java里设置一堆getter/setter很繁琐啰嗦,好处就是为你提供了封装,

  • 封装是软件得以成功演进的保证

设计模式:抽象工厂方法

推荐一本书 effective java,无论处在java任何阶段都非常值得一读

  • 使用静态工厂方法代替构造器
  • 将构造器私有化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Cat newCuteCat(String name){
return new Cat(name,true);
}

public static Cat newUnCuteCat(String name){
return new Cat(name,false);
}

public Cat(String name, boolean cute) {
this.name = name;
this.cute = cute;
}

// 静态工厂方法,可以有个名字,清楚无误的告诉你干什么
Cat.newCuteCat("xxx") // 萌的猫
Cat.newUnCuteCat("xxx") // 不萌的猫
new Cat("xxx",true) // 看不出来
优点
  1. 不像构造器,它是有名字的。可以描述在做什么
    • 疑问,我可以通过注释来告诉别人构造器做什么
    • 注释是不会被编译器处理的,因此它很有可能过时,过时的注释很可能会误导你,一个过时的注释比没有注释更糟糕
    • 尽可能不要写注释,如果你不能保证及时更新
  2. 静态方法不一定创建一个实例,你可以返回一个null也可以返回一个之前创建好的对象,但是构造器一定会创建一个实例
  3. 静态构造方法可以返回 该类的子类型,而构造器只能返回该类的实例
  4. 可以根据参数决定 要不要创建这个对象,我要创建什么对象,以及要不要把之前的对象缓存一下

    • 参考Boolean

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public final class Boolean implements java.io.Serializable,
      Comparable<Boolean>
      {
      // 预先定义好的对象
      public static final Boolean TRUE = new Boolean(true);
      public static final Boolean FALSE = new Boolean(false);

      public static Boolean valueOf(boolean b) {
      return (b ? TRUE : FALSE);
      }
      ...
      }
    • 它返回了预先定义好的对象,不用每次都创建,省内存

  5. 静态工厂返回的这个对象,它可以不存在
    • 动态加载,灵活的体现
缺点
  1. 一个子类构造器会自动调用父类的构造器,构造对应的对象,但是静态方法不可以
  2. 很难让开发者找到,因为它的灵活性只能打开文档找到它 而不像这样new Cat()方便找到
静态方法的最佳实践
  • 将构造器变为私有,此时外界无法创建实例,只能通过暴露的工厂方法
  • 此时你内部如何修改构造器都随意了。外界只能操作暴露的工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Cat {

private String name;
private boolean cute;

public static Cat newCuteCat(String name){
return new Cat(name,true);
}

public static Cat newUnCuteCat(String name){
return new Cat(name,false);
}

private Cat(String name, boolean cute) {
this.name = name;
this.cute = cute;
}
}

类的访问控制

这就是封装在类级别的表现

1
2
3
4
5
6
7
8
9
// 之前,不同包可以直接使用
public class Cat{

}

// 现在 包级私有 package private
class Cat{

}
  • 包级私有类 如ProcessEnvironment 在其他包外不能调用。

如何访问一个 包级私有的类呢?(不建议使用太hack)

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
// 假设一个类,它是 包级私有 只能同包访问
package com.github.demo;
class A{}


// 在你的 maven项目里创建一个 桥接类 ,它的包路径和 上面的 一样
package com.github.demo;
class 桥接类{
public A newInstance(){
// 访问同一包中的私有类
return new A();
}
}

// 此时你去创建这个类
桥接类.newInstance();// 报错 因为java发现返回值 还是不能访问

// 怎么办呢,修改返回值类型为 Object
class 桥接类{
public Object newInstance(){
// 访问同一包中的私有类
return new A();
}
}

// 因为 任何对象都是 Object 的子类
// 此时你就可以
桥接类.newInstance();

既然这样那么我们是不是可以这样绕过限制创建 ProcessEnvironment 的实例呢?

1
2
3
4
5
6
// 你就创建这样的package
package java.lang;
public class MyClass{}
//此时报错了。 说这个类是被禁止的。

原因是 以 java开头的包都是 jvm的保留包,不允许你自定义一个java.lang包的,但是你可以通过别的包的访问限制

私有内部类

一个类有简单功能,你不想几个目录跳过来跳过去的使用

1
2
3
4
5
6
7
public class Home{

// 只能在同一个类中访问
private static class InnerClass{

}
}

Java模块系统简介

需求:你像把若干个包封装在一起,暴露接口出去

  • java8之前是不行的
  • java9引入了模块化系统,你可以把包封装成模块 module

java9的模块化系统好处是提供了更大范围的封装。但是它太新了。没有被业界所接受。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
假设你是管理者,你手下有工人
package com.farm;
public class Manger{
private Worker worker;

public void manage(){
worker.work();
}
}

package com.farm;
public class Worker{
public void work(){
}
}

// 此时你的类发布了,此时你不小心暴露了 worker

另一个项目中,利用同包路径来访问你的worker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.farm;
public class Boss{
Manager manager;

public void runCompany(){
manager.manage();
}

// Boss直接指挥工人,此时 manager就懵逼了,你怎么直接指挥工人了
public void directWorker(){
Worker w1 = new Worker();
w1.work();
}
}

为了不让 Boss 直接指挥 worker

1
2
3
4
// 你只能包级私有
class Worker{

}

但是有时候,出于其他原因你不得不把它自己包里面

  • 导致 manager 无法指挥 worker了
  • 只能 public了,一旦 public 你的老板又开始指挥工人了
  • 我们只能使用一些君子协定如 internal 包名 让人知道是内部的包。但是别人不君子咋办!!!
    1
    2
    3
    4
    package com.farm.worker.internal;
    public class Worker{
    ...
    }
1
2
3
4
package com.farm.worker;
class Worker{

}

java8之前无法做到,技术上做不到
java9的模块化系统可以解决,但是太新了,业界还没接受

builder模式

  • 简略版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.json;

public class Person {
private String firstName;
private String lastName;
private String desc;
private String job;
private String phone;
private String address;

public Person(String firstName, String lastName, String desc, String job, String phone, String address) {
this.firstName = firstName;
this.lastName = lastName;
this.desc = desc;
this.job = job;
this.phone = phone;
this.address = address;
}
}
  • 一个 Person 有诸多属性你在创建的时候,顺序错了非常难发现,
  • 尤其是在 代码 review 的时候,不再 IDEA 里 ,没有参数提示。
  • 这个时候你可以使用 builder 安装 builder
  • 在IDEA里右键就可以创建 builder

此时可以这样

1
2
3
4
5
6
7
8
Person person1 = new Person("","","","","","")

// 更加直观,链式调用
Person person = PersonBuilder.aPerson()
.withFirstName("")
.withLastName("")
.withAddress("")
.build();