(注:能力有限,如有说错,请指正!)
扩展:OOP 编程
这是一个非常有价值有深度的问题,我们可以来讨论一下。
OOP 强调封装,也就是把内部实现隐藏,但是内部不一定包含数据。
我们来看一个例子:
public class Entity {
private static final String PREFIX = "Hello";
private static final String SUFFIX = "World";
public String getName() {
return PREFIX + SUFFIX;
}
}
由于多了一层方法的封装,所以内部数据更改了,外界可以不用去感知,
甚至还傻傻的以为内部真的有 name 属性。
这就是封装的好处。
OOP 中有继承,但是继承一直被人诟病,甚至于在 Golang 中完全被阉割,取而代之的是组合。
继承不是说不好,而是会将整个类的体系变得非常庞大而且难于理解,简单的系统还好,如果是成百上千个类的继承关系,
那就等着哭吧,维护的工作将变得非常困难,而且对于新入职的员工来说难于理解。
另外,我个人觉得继承不好还有一个原因就是在于意义层面。举个例子,小明的父亲可以借钱给别人。
class XiaoMing extends XiaoMingFather {}
class XiaoMingFather {
public double borrow(double numberOfMoney) {
// 你说要借多少,他就借多少,小明父亲就是这么豪
return numberOfMoney;
}
}
当我们要向小明借钱的时候,相当于调用 xiaoMing.borrow(666.666),
然而,这个方法并不属于小明,它是小明父亲的方法,我们可以看到小明的类里面并没有直接声明这个方法,
所以这相当于我们直接向小明父亲借钱,而我们根本不认识小明父亲,这不符合逻辑。
如果改用组合呢?
class XiaoMing {
private XiaoMingFather father = new XiaoMingFather();
public double borrow(double numberOfMoney) {
// 你说要借多少,他就借多少,有其父必有其子
return father.borrow(numberOfMoney);
}
}
class XiaoMingFather {
public double borrow(double numberOfMoney) {
// 你说要借多少,我就借多少,小明父亲就是这么豪
return numberOfMoney;
}
}
很明显可以看到代码不一样了,我们在小明内部镶嵌了一个小明父亲,而我们向小明借钱的时候,
是直接调用的小明的方法,而不是小明父亲的方法,虽然在小明内部还是调用了小明父亲的方法,
但是对于外界我们是不知情的,我们只认识小明,也只会跟小明借钱,
至于小明找谁拿的钱,我们不在乎,我们只在乎能不能拿得到钱。。。
可以看得出,组合相比继承更具逻辑性,而且我们可以看到小明和小明父亲没有直接的联系,就是说小明内部可以随时更换借钱的人,
只需要很简单的换个父亲即可(只要小明愿意),如果是继承,这就不好办了,由于我们直接调用的是小明父亲的方法,
所以如果小明要换一个父亲,调用方就需要随着改变。
与其说多态是 OOP 的精髓,不如说多态是面向接口的精髓。当我们在 Java 中使用多态的时候,
要不就是使用继承,要不就是使用接口。
我们来看一个例子:
interface Walkable {
void walk();
}
class Human implements Walkable {
public void walk() {
System.out.println("我是人类,直立行走!");
}
}
class Dog implements Walkable {
public void walk() {
System.out.println("我是狗狗,四条腿走!");
}
}
class Entity {
public int walk(Walkable walkable) {
return walkable.walk();
}
}
当我们调用 Entity::walk 方法时,你可能是这样的:walk(new Human()),也有可能是这样的:
walk(new Dog())。不管哪一种方式,执行结果都会不一样,因为这就是多态的价值,
同样的调用会带来不一样(多种状态)的结果。
那我们反过来想,对于 Entity 的 walk 方法来说,它不用去关心具体实现,也没必要去关心,
因为不管是哪一种结果,对它都没有差别,因为都是走。
多态对于很多代码设计都有很大的价值,我知识有限,就点到为止吧。
public class Test {
@Override
protected void finalize() throws Throwable {
super.finalize();
// 不建议重写这个方法!!!
// do something...
// 在垃圾收集器执行的时候会调用被回收对象的此方法
// 但是,需要注意,这个方法并不保证一定会被执行
}
}
扩展:包装类的意义
基本类型在对大部分需求已经足够了,而且相比包装类更省内存,但是基本类型没有办法表达空值。
比如,一个同学缺考了,要怎么记录这个分数?
可以用 -1,这是一种办法,但是语义不够清楚,如果不说,你可能得猜猜这代表什么意思,
另外,如果对一个需求来说整个范围都是有意义的呢,那用什么来表示空值?这时候包装类就可以很好的表示了,
毕竟 null 不就是空了吗。最主要的是,一些集合类,比如 List 没办法用基本类型。。。这是一个硬伤。
扩展:两者的区别
在 JDK7 以前的版本,两者的区别还比较大,比如接口不能有默认实现,
但是在 JDK8 之后,情况就变了,JDK 官方似乎一直在模糊两者的边界,让接口也可以有默认实现,
只需要使用 default 关键字:
interface Coder {
default void code() {
System.out.println("coding...");
}
}
在很多的框架设计中,一般都会有这样的设计,首先使用接口,比如上面的 Coder 接口,
然后创建一个抽象类,比如 AbstractCoder,然后再有具体的是实现类,比如 JavaCoder。
接口一般不会设计的太复杂,通常只有一个方法,这样可以保证接口的简洁,在维护上也方便,用户实现也方便很多。
扩展:反射的性能
虽然反射可以做很多灵活的操作,但这是有代价的 ———— 性能。
我们来看看使用反射之后性能差了多少:
class ReflectTest {
private String value = null;
public static void main(String[] args) throws Exception {
ReflectTest test = new ReflectTest();
double beginTime = System.currentTimeMillis();
withoutReflect(test); // 380 ms 左右
double endTime = System.currentTimeMillis();
System.out.println("withoutReflect: " + (endTime - beginTime) + " ms.");
beginTime = System.currentTimeMillis();
withReflect1(test); // 1600 ms 左右
endTime = System.currentTimeMillis();
System.out.println("withReflect: " + (endTime - beginTime) + " ms.");
beginTime = System.currentTimeMillis();
withReflect2(test); // 390 ms 左右
endTime = System.currentTimeMillis();
System.out.println("withReflect: " + (endTime - beginTime) + " ms.");
}
// 不使用反射
private static void withoutReflect(ReflectTest test) {
for (int i = 0; i < 10000000; i++) {
test.value = String.valueOf(i);
}
}
// 使用反射
private static void withReflect1(ReflectTest test) throws Exception {
for (int i = 0; i < 10000000; i++) {
Field field = ReflectTest.class.getDeclaredField("value");
field.setAccessible(true);
field.set(test, String.valueOf(i));
}
}
// 使用反射
private static void withReflect2(ReflectTest test) throws Exception {
Field field = ReflectTest.class.getDeclaredField("value");
field.setAccessible(true);
for (int i = 0; i < 10000000; i++) {
field.set(test, String.valueOf(i));
}
}
}
很明显,使用反射之后性能有所下降,执行时间变成了原来的四倍,
但是对比 withReflect1 和 withReflect2 可以发现,获取反射值才是最耗时的,
也就是说,如果是使用反射修改值,提前获取到反射数据的话,性能下降很少很少。
总的来说,使用反射之后,性能有所下降,但是这个差距要在数量级非常庞大才能体现,
如果量级只有几十万次执行,这个下降就不算太大的影响,而且做了预处理之后,性能几乎没有下降,
相比反射带来的便利,显然性能下降的那一点点也就可以接受了。
扩展:RestFul 风格
HTTP 中有多种请求方式,每一种其实都是有用的,尤其是在语义上,会有很大的区别。
而 RestFul 也仅仅是一种风格,并不是具体的标准或者要求,它就是建议请求使用符合语义的请求方法。
GET 请求,用于获取数据,有些浏览器会对这个请求方式的请求进行缓存,
所以如果是一个修改数据的请求使用了 GET 请求,就有可能导致这个请求被缓存,
导致下一次请求失效,但是这个情况很少很少,除了会被缓存之外,由于 GET 请求使用 url 来传递参数,
所以参数会被直接暴露在 url 中,也就有了它不安全的说法,还有部分浏览器会对 url 长度做限制,这时候 GET 请求中的数据就会被限制。
POST 请求,用于保存数据,和 GET 不同的是,请求数据会被隐藏到请求体中,也就是人们所说的“安全”,
但是这还是明文传输的,如果真的需要加密,应该考虑的是 HTTPS 而不是将 GET 更换为 POST,
你甚至可以对数据进行对称或者非对称加密然后再传输
PUT 请求,用于修改数据,这个请求方式在 SpringMVC 中需要配置一个参数才能正常使用,
具体参考 SpringMVC 的用法,这里不多说
DELETE 请求,用于删除数据
扩展:分布式 session 问题
当一个系统拆分到多台服务器之后,就存在多台服务器之间的会话共享问题,
比如现在有 3 台服务器,ABC,我在 A 服务器上登录了,B 服务器如何感知我已经登录了就是分布式系统需要解决的一个题,
这就是分布式 session 问题,解决办法其实有很多,下面列举几种来聊聊
上面大概聊了几种实现分布式 session 的思路,最经常用也是最推荐的是基于 Cookie/sessionStorage 管理的
Session 集中管理,也就是上面 3,4 点结合起来用,在 Cookie/sessionStorage 中存的不是用户信息,
而是用户本次登录的凭证,这个凭证由服务器生成并保存在前台,每一次请求都会带上凭证,这样服务器就可以检验并获取到会话信息,同样的,这个凭证也会涉及到伪造的问题,所以一般都会使用特定的加密算法对用户特定信息做指纹处理,生成特殊的用户登录凭证,以防止伪造,比如简单使用用户某些信息作 MD5 加密,又或者是使用 JWT 生成
class Entity {
private String name = null;
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
// 这里使用 Class 来比较而不是用 instanceof 是因为:
// instanceof 会把子类当成父类,也就是 "str" instanceof Object ,将返回 true
if (this.getClass() == o.getClass()) return name.equals((Entity(o)).name);
return false;
}
}
今天就到这里!晚安!