一.Java设计模式的六大设计原则
1.单一职责:一个类只负责一项职责
遵循单一职责原的优点有:
可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
提高类的可读性,提高系统的可维护性;
变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
2.里氏替换:子类可以扩展父类的功能,但不能改变父类原有的功能
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。【注意区分重载和重写】
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
3.依赖倒置:要求对抽象、接口进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合
低层模块尽量都要有抽象类或接口,或者两者都有。【可能会被人用到的】
变量的声明类型尽量是抽象类或接口。
使用继承时遵循里氏替换原则。
4.接口隔离:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注的为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
5.迪米特:最少知道法则,一个对象应该对其他对象保持最少的了解
一个类对自己依赖的类知道的越少越好。
对于被依赖的类来说,无论逻辑多么复杂,都尽量的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息
从依赖者的角度来说,只依赖应该依赖的对象。
从被依赖者的角度说,只暴露应该暴露的方法。
6.开闭:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
用抽象构建框架,用实现扩展细节的注意事项而已:
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合。
而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
二.Java虚拟机
public class Test {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
// 执行代码
public static void main(String[] args) {
Human man = new Man();
// 变量man的静态类型 = (引用类型 = Human):不会被改变、在编译器可知
// 变量man的动态类型 = (实例对象类型 = Man):会变化、在运行期才可知
}
}
变量的静态类型 = 引用类型 :不会被改变、在编译器可知
变量的动态类型 = 实例对象类型 :会变化、在运行期才可知
1.静态分派
定义:根据变量的静态类型进行方法分派的行为
即根据变量的静态类型确定执行哪个方法,发生在编译期,所以不由 Java 虚拟机来执行
应用场景:方法重载(OverLoad)
public class Test {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
// 可供重载的方法
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello lady!");
}
// 测试代码
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test test = new Test();
test.sayHello(man);
test.sayHello(woman);
}
}
运行结果(可通过 强制类型转换 改变 变量的静态类型):
hello,guy!
hello,guy!
2.动态分派
定义:根据变量的动态类型进行方法分派的行为
即根据变量的动态类型确定执行哪个方法,发生在运行期,由 Java 虚拟机来执行
应用场景:方法重写(Override)
// 定义类
class Human {
public void sayHello(){
System.out.println("Human say hello");
}
}
class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
// 测试代码
public static void main(String[] args) {
// 情况1
Human man = new man();
man.sayHello();
// 情况2
man = new Woman();
man.sayHello();
}
运行结果:
man say hello
woman say hello
3.类加载
a. 类加载的本质
将描述类的数据 从Class文件加载到内存&对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java使用类型,Class文件是一串二进制字节流
b. 类加载过程
分为五个步骤:加载 -> 验证 -> 准备 -> 解析 -> 初始化
c. 类加载器
启动类加载器:这个类加载器负责将存放在JAVA_HOME/lib下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用;
扩展类加载器:这个加载器负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;
应用程序类加载器:这个加载器是ClassLoader中getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器
d.双亲委派模型 【流程代码实现在java.lang.ClassLoader的loadClass()中 】
若一个类加载器收到了类加载请求,把 该类加载请求 委派给 父类加载器去完成,而不会自己去加载该类,每层的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载。
这种设计模式的好处在于:
1、保证class只会被加载一次:也就是说类的数据结构只会在第一次创建的时候被加载进内存(方法区),以后要创建这个类的对象的时候,直接用方法区中的class在堆内存创建一个对象即可,这样的话创建对象就会比较快;
2、保证系统类的安全性:因为在启动应用进程的时候就已经加载好了系统类(BootClassLoader),那后面运行期就不可能通过恶意伪造加载的方式去造成一些系统安全问题;
4.Java虚拟机内存结构
jvm-class执行图.pngJava虚拟机在运行Java程序时,会管理着一块内存区域:运行时数据区,在运行时数据区里,会根据用途进行划分:
Java虚拟机栈(栈区):存放java方法执行的局部变量
本地方法栈:native方法
Java堆(堆区):存放java对象实例
方法区:存放常量、静态变量等数据
程序计数器:线程私有的内存区域,实现异常处理及线程恢复
5.Java虚拟机垃圾收集算法
a.标记-清除 算法
垃圾收集算法中 最最基础的算法。
1.1 算法思想
算法分为两个阶段:
标记阶段:标记出所有需要回收的对象;
清除阶段:统一清除(回收)所有被标记的对象。
1.2 优点
算法简单、实现简单
1.3 缺点
效率问题:即 标记和清除 两个过程效率不高
空间问题:标记 - 清除后,会产生大量不连续的内存碎片。
1.4 应用场景
对象存活率较低 & 垃圾回收行为频率低 的场景
如老年代区域,因为老年代区域回收频次少、回收数量少,所以对于效率问题 & 空间问题不会很明显。
b.复制算法
该算法的出现是为了解决 标记-清除算法中 效率 & 空间问题的。
2.1 算法思想
将内存分为大小相等的两块,每次使用其中一块;
当使用的这块内存 用完,就将 这块内存上还存活的对象 复制到另一块还没试用过的内存上,最终将使用的那块内存一次清理掉。
2.2 优点
解决了标记-清除算法中 清除效率低的问题,每次仅回收内存的一半区域,解决了标记-清除算法中 空间产生不连续内存碎片的问题,将已使用内存上的存活对象 移动到栈顶的指针,按顺序分配内存即可。
2.3 缺点
每次使用的内存缩小为原来的一半。
当对象存活率较高的情况下需要做很多复制操作,即效率会变低
2.4 应用场景
对象存活率较低 & 需要频繁进行垃圾回收 的区域
如新生代区域
c.标记 - 整理 算法
此算法类似于第一种标记 - 清除 算法,只是在中间加多了一步:整理内存。
3.1 算法思路
算法分为三个阶段:
标记阶段:标记出所有需要回收的对象;
整理阶段:让所有存活的对象都向一端移动
清除阶段:统一清除(回收)端以外的对象。
3.2 优点
解决了标记-清除算法中 清除效率低的问题:一次清楚端外区域
解决了标记-清除算法中 空间产生不连续内存碎片的问题:将已使用内存上的存活对象 移动到栈顶的指针,按顺序分配内存即可。
3.3 应用场景
对象存活率较低 & 垃圾回收行为频率低 的场景
如老年代区域,因为老年代区域回收频次少、回收数量少,所以对于效率问题 & 空间问题不会很明显。
d.分代收集算法
主流的虚拟机基本都采用该算法,下面会着重讲解。
4.1 算法思路
根据对象存活周期的不同 将 Java堆内存 分为:新生代 & 老年代 。分配比例:1(8(eden):1(survivor):1(survivor)):2
4.2 优点
效率高、空间利用率高
根据不同区域特点 选择不同的垃圾收集算法
4.3 应用场景
现在主流的虚拟机基本都采用 分代收集算法 ,即根据不同区域特点选择不同垃圾收集算法。
新生代 区域:采用 复制算法
老年代 区域:采用 标记-清除 算法、标记 - 整理 算法
①年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
②年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
③持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。
一个对象的这一辈子
我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
6.判断一个Java对象是否存活
a.判断方式
垃圾收集器对Java堆里的对象是否进行回收的判断准则:Java对象是存活 or 死亡,判断对象为死亡才会进行回收
b.在Java虚拟机中,判断对象是否存活有2种方法:
引用计数法
引用链法(可达性分析法)
Java里头有GC垃圾回收机制,他是怎么判断该不该回收呢?
Q1:某对象没有任何引用的时候才进行回收?
A:不。无法往上追溯到GCroot引用点的时候就回收[比如:上图中的G、H被F引用,但是F没有被GcRoots引用]
Q2:某对象被别的对象引用就不能进行回收?
A:不。软引用,弱引用,虚引用就可以回收
一句话理解GC Roots
假设我们现在有三个实体,分别是 人、狗、毛衣,然后他们之间的关系是:人牵着狗,狗穿着毛衣,他们之间是强连接的关系,有一天人消失了,只剩下狗狗和毛衣,这个时候,把人想象成 GC Roots,因为人和狗之间失去了绳子连接,那么狗可能被回收,也就是被警察抓起来,被送到流浪狗寄养所,假设狗和人有强连接的时候,狗狗就不会被当成是流浪狗。
哪些可以作为GCroot引用点:
1.Java stack中引用的对象
public class Rumenz{
public static void main(String[] args) {
Rumenz a = new Rumenz();
a = null;
}
}
a是栈帧中的本地变量,a就是GC Root,由于a=null,a与new Rumenz()对象断开了链接,所以对象会被回收。
2.方法区中静态引用指向的对象
public class Rumenz{
public static Rumenz r;
public static void main(String[] args){
Rumenz a=new Rumenz();
a.r=new Rumenz();
a=null;
}
}
栈帧中的本地变量a=null,由于a断开了与GC Root对象(a对象)的联系,所以a对象会被回收。由于给Rumenz的成员变量r赋值了变量的引用,并且r成员变量是静态的,所以r就是一个GC Root对象,所以r指向的对象不会被回收。
3.方法区中常量引用指向的对象
public class Rumenz{
public static final Rumenz r=new Rumenz();
public static void main(String[] args){
Rumenz a=new Rumenz();
a=null;
}
}
常量r引用的对象不会因为a引用的对象的回收而被回收。
4.Native方法中JNI引用指向的对象
JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
// 缓存String的class
jclass jc = (*env)->FindClass(env, STRING_PATH);
//实例化该类
jobject job = env->AllocObject(jc);
}
5.Thread-活着的线程
https://www.cnblogs.com/tangZH/p/10955429.html
网友评论