回顾基础知识过程中遇到的感觉需要记录一下的知识点。

封装

我们设计的class应当尽可能的高内聚,体现为封装的程度。一个class的属性应该只能自己修改,其他class都只是与本class沟通,而不应该有能力修改。比较常见的一个问题是Date属性。

业务开发中经常需要设计class的日期属性,比如birthday, createDate等。

1
2
3
4
5
6
7
8
public class User {
private String name;
private Date birth;

public Date getBirth() {
return this.birth;
}
}

我经常设计一个entity,填入字段,然后就直接getter, setter出去,尤其使用lombok后,更是连生成都改自动了。按照封装的要求,这样的做法是不合适的。因为其他class可以获取Date对象,Date对象是可变的。那么,就有可能会产生日期被修改的可能。

如果项目有引入Findbugs的扫描,这个class肯定会被扫描出来的,不应该返回一个可变对象。那么,怎么做才可以避免这个问题?我们确实需要暴露Date啊。

答案是暴露一个不可变的对象。Java8推出了新的日期API,其中LocalDate就是不可变的。用LocalDate替换Date即可。LocalDate就像String一样,没有提供任何可以改变内部属性的方法,所有的修改之类的方法都将会创建一个新的对象。这样,修改操作将不会影响原来的class。

Date对象可以解决,但很多是自己定义的对象,这个怎么办?比如,User有个属性是Role。

1
2
3
4
5
6
7
8
9
public class User {
private String name;
private LocalDate birth;
private Role role;

public Role getRole() {
return this.role;
}
}

和Date类似,当调用getRole之后,外界如果可以修改role的属性,比如把role改成admin,那么本对象就拥有了admin权限了。这是我们不愿看到的。可以模仿LocalDate,把Role的所有修改内部属性的方法关闭。这样,外部无法修改Role,就不会影响到User了。然而,我们web中需要把对象渲染成json发送出去,jackson会根据getter setter去做序列化和反序列化操作。这个setter还不能关闭。

那就只能处理User自己了。user唯一发生风险的地方在于getRole后,把自己内部属性暴露出去了。我们可以getRole的时候给一个新的出去,让其他class随便改都不会影响自己。

1
2
3
4
5
6
7
8
9
public class User {
private String name;
private LocalDate birth;
private Role role;

public Role getRole() {
return (Role) this.role.clone();
}
}

现实是,我们很少关注这样的做法,都是直接返回。而且,也很少遇到错误。这时候可以 忽略findbugs的异常。但,最好的,还是推荐做这样的修改。

方法传参的按值调用

初学Java的时候最容易搞不懂的地方就是传递参数到底是怎么传递的。

在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call be reference)**表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。按…调用(call by)**是一个标准的计算机科学术语,它用来描述各种程序设计语言(不只Java)中方法参数的传递方式。

Java程序设计语言总是按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

1
2
int a = 10;
addOne(a)

不管addOne方法具体实现,a最终依旧还是10. 因为当a传递给addOne方法的时候,拷贝了一份a的值给参数,方法中运行过程中都是一份拷贝,不会影响原来的变量。

方法参数共有两种:

  • 基本数据类型(数字、布尔值)
  • 对象引用

上述demo显示一个方法不可能修改一个基本数据类型的参数。那么对象引用呢?

1
2
3
StringBuilder sb = new StringBuilder();

appendOne(sb);

appendOne执行过程中会对sb产生影响吗?

这就要看具体方法内容了。比如

1
2
3
4
public void appendOne(StringBuilder sb) {
sb.append("1");
}

那么,我们最终执行完结果肯定sb内容添加了1。而换一个方式,

1
2
3
4
public void appendOne2(StringBuilder s){
s = new StringBuilder();
s.append("a");
}

这样,方法外面的sb的内容会变成什么?

这个,首先要记住的是Java方法传参都只是传递拷贝。然后,明白传递拷贝的意义

因为s指向的地址和sb相同,故,当s.append的时候,sb的内容也会改变。这也上开头讲述的封装不应返回一个可变变量的原因。任何拿到这个可变变量地址的方法都可以直接修改变量里的属性。那方法2有什么不同?

方法2中,把参数s指向了新地址,那么接下来的任何修改,都将不会影响旧地址。则方法外的sb对应的地址空间也就不会发生变化。这个可以理解为Java传递对象引用的时候只复制了对象引用的地址。

类设计技巧

  1. 一定要保证数据私有,即封装性;
  2. 一定要对数据初始化,最好不要依赖系统的默认值,自己给定一个初始值;
  3. 不要在类中使用过多的基本类型,可以把相关的几个变量合成一个class,转为引用class,另外,能用包装类就不用基本类型;
  4. 不是所有的成员变量都应该提供对外访问方法,比如创建日期不可以修改;
  5. 将职责过多的类进行分解;
  6. 类名和方法名要能够体现他们的职责;
  7. 优先使用不可变的类。