2019-5-17
作者:水不要鱼
(注:能力有限,如有说错,请指正!)
- 封装 (基础)
- 把一些内部实现隐藏,只公开一部分“想公开”的内容。
- 外界只需要使用这部分公开的内容即可,不需要关心内部实现
- 继承 (实现多态的一种手段)
- 子承父业,没啥好说的
- 多态 (OOP 的精髓)
- 一个标准可以有多个实现,而为了解决代码之间的耦合性,我们不会在代码中直接引入具体的标准实现。
扩展: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 方法来说,它不用去关心具体实现,也没必要去关心,
因为不管是哪一种结果,对它都没有差别,因为都是走。多态对于很多代码设计都有很大的价值,我知识有限,就点到为止吧。
-
final, finally, finalize 的区别
- final: 修饰类的关键字
- finally: 异常处理块
- finalize: 这是 Object 的一个方法
public class Test { @Override protected void finalize() throws Throwable { super.finalize(); // 不建议重写这个方法!!! // do something... // 在垃圾收集器执行的时候会调用被回收对象的此方法 // 但是,需要注意,这个方法并不保证一定会被执行 } }
- 基本类型不可以有空值
- 包装类型可以有空值
扩展:包装类的意义
基本类型在对大部分需求已经足够了,而且相比包装类更省内存,但是基本类型没有办法表达空值。
比如,一个同学缺考了,要怎么记录这个分数?
可以用 -1,这是一种办法,但是语义不够清楚,如果不说,你可能得猜猜这代表什么意思,
另外,如果对一个需求来说整个范围都是有意义的呢,那用什么来表示空值?这时候包装类就可以很好的表示了,
毕竟 null 不就是空了吗。最主要的是,一些集合类,比如 List 没办法用基本类型。。。这是一个硬伤。
- 重载是一个类中多个方法名字相同,但是参数列表不相同
- 名字一样,参数列表不同
- 返回值呢?返回值不能用来区分方法,也就是说两个方法,只有返回值不一样的话,
这两个是属于同一个方法
- 重写(覆盖)是子类写了一个父类中已经存在的方法
- 注意:子类和父类,这就存在一层上下级的关系了,这个关系除了继承,还可以是接口的实现
- 由于 Java 中默认多态,所以使用父类引用(这个父类引用指向的是子类对象)时,可以调用到子类的实现
- 抽象类,顾名思义是一个类
- 接口,是一种协议或者标准
扩展:两者的区别
在 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 可以发现,获取反射值才是最耗时的,
也就是说,如果是使用反射修改值,提前获取到反射数据的话,性能下降很少很少。
总的来说,使用反射之后,性能有所下降,但是这个差距要在数量级非常庞大才能体现,
如果量级只有几十万次执行,这个下降就不算太大的影响,而且做了预处理之后,性能几乎没有下降,
相比反射带来的便利,显然性能下降的那一点点也就可以接受了。
- 目前常用的有 GET/POST/PUT/DELETE/HEAD/OPTIONS
- 每一种方式在使用的时候都应该有对应的意义
扩展:RestFul 风格
HTTP 中有多种请求方式,每一种其实都是有用的,尤其是在语义上,会有很大的区别。
而 RestFul 也仅仅是一种风格,并不是具体的标准或者要求,它就是建议请求使用符合语义的请求方法。
-
GET 请求,用于获取数据,有些浏览器会对这个请求方式的请求进行缓存,
所以如果是一个修改数据的请求使用了 GET 请求,就有可能导致这个请求被缓存,
导致下一次请求失效,但是这个情况很少很少,除了会被缓存之外,由于 GET 请求使用 url 来传递参数,
所以参数会被直接暴露在 url 中,也就有了它不安全的说法,还有部分浏览器会对 url 长度做限制,这时候 GET 请求中的数据就会被限制。 -
POST 请求,用于保存数据,和 GET 不同的是,请求数据会被隐藏到请求体中,也就是人们所说的“安全”,
但是这还是明文传输的,如果真的需要加密,应该考虑的是 HTTPS 而不是将 GET 更换为 POST,
你甚至可以对数据进行对称或者非对称加密然后再传输 -
PUT 请求,用于修改数据,这个请求方式在 SpringMVC 中需要配置一个参数才能正常使用,
具体参考 SpringMVC 的用法,这里不多说 -
DELETE 请求,用于删除数据
-
session 与 cookie
- 某种程度上说,这两个毛关系都没有,Session 是服务端对会话的标准或者说接口,
比如 Java 中的 HttpSession 就是这种标准的产物,而 Cookie 是浏览器的一种存储载体,
和 localStorage 和 sessionStorage 是相同功能的东西,并不是用来作会话的,
只是说,你可以将会话信息存在 Cookie 中,以达到会话的功能,而这个功能,
你使用一个 js 变量就可以达到,甚至还更方便更简单,不过很多人把他们混为一体来谈,
下面就是以混为一体来谈为前提 - cookie 数据存放在客户的浏览器上,session 数据放在服务器上
- cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗,
考虑到安全应当使用 session。 - session 会在一定时间内保存在服务器上,也就是有存活周期。
- 单个 cookie 保存的数据不能超过 4KB,而且很多浏览器对 Cookie 的存储数量有限制。
- 客户端是可以关闭 Cookie 的
扩展:分布式 session 问题
当一个系统拆分到多台服务器之后,就存在多台服务器之间的会话共享问题,
比如现在有 3 台服务器,ABC,我在 A 服务器上登录了,B 服务器如何感知我已经登录了就是分布式系统需要解决的一个题,
这就是分布式 session 问题,解决办法其实有很多,下面列举几种来聊聊
- Session 复制:将 A 服务器上的会话信息复制到 BC 服务器上,这样 BC 服务器也就有了我的登录信息,
这样做就多了一次同步,在请求数很多的时候会占用很大的网络带宽,因为 A 服务器一直在给 BC 服务器发送当前的会话信息。
而且,同步就会有延迟,除非你在 A 服务器上等待 BC 服务器同步完成才返回给客户,否则就会出现短暂的不同步问题,
比如我在 A 登录了,在 A 上的会话信息还没同步到 BC 上之前,我访问 BC 服务器都会是处于未登录状态 - Session 粘滞:将用户和某台服务器进行绑定,也就是这个用户所有的请求都交给这台服务器处理,这样即使在 BC 上没有
登录信息也没关系,因为根本不会交给它们处理。但是这样的话就会把应用服务器变成是有状态的,对后期扩展不方便,除此之外,如果 A 服务器绑定的用户比 BC 服务器上的用户要活跃的多,就会导致 A 服务器的负载很高,而 BC 都在空闲 - Session 集中管理:将会话信息统一保存到一台独立的服务器上,比如将会话信息保存在 Redis 中,
所有的Web服务器都从这个 Redis 中存取对应的 Session,实现 Session 共享。你甚至可以将数据持久化保存在数据库或者文件系统中,不过由于引入了独立的服务器,开发和维护当然就更多事情要处理了,不过这个代价是值得的 - 基于 Cookie/sessionStorage 管理:将会话信息保存在浏览器本地或者内存中,每一次发请求都携带会话信息(比如将信息保存在请求头中)。这种方式极其不安全,因为请求是可以被伪造的,而且如果信息太多还容易造成 HTTP 请求头太庞大和信息泄露。
上面大概聊了几种实现分布式 session 的思路,最经常用也是最推荐的是基于 Cookie/sessionStorage 管理的
Session 集中管理,也就是上面 3,4 点结合起来用,在 Cookie/sessionStorage 中存的不是用户信息,
而是用户本次登录的凭证,这个凭证由服务器生成并保存在前台,每一次请求都会带上凭证,这样服务器就可以检验并获取到会话信息,同样的,这个凭证也会涉及到伪造的问题,所以一般都会使用特定的加密算法对用户特定信息做指纹处理,生成特殊的用户登录凭证,以防止伪造,比如简单使用用户某些信息作 MD5 加密,又或者是使用 JWT 生成
- equals 和 ==
- == 是 Java 的一个操作符,equals 是 Object 类中的一个方法
- equals 默认比较的就是地址,所以对于引用类型来说,和 == 作用是一样的
- 通常都会重写这个方法用于比较具体的内容
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;
}
}
今天就到这里!晚安!