模板设计模式
定义一个操作中的算法骨架,将通用步骤以模板形式封装在父类模板中,将具体细节步骤延迟到子类中实现。
使用场景:
适用于一些复杂操作进行步骤分割、抽取公共部分由抽象父类实现、将不同的部分在父类中定义抽象实现、而将具体实现过程由子类完成。对于整体步骤很固定,但是某些部分易变,可以将易变的部分抽取出来,供子类实现。
角色:
-
抽象类:实现模板方法、定义算法骨架;
-
具体类:实现抽象类中的抽象方法,完成特定的算法。
案例:将物品放入冰箱,需要几步
- 打开冰箱
- 将物品放入
- 关上冰箱
父类TemplateClass
package com.blb.demo8;
/**
* 父类
* 模板类
* @author admin
*
*/
public abstract class TemplateClass {
//打开冰箱
private void open(){
System.out.println("打开冰箱");
}
//放入物品
protected abstract void put();
//关闭冰箱
private void close(){
System.out.println("关闭冰箱");
}
/**
* 将物品放入冰箱
*/
public void execute(){
open();
put();
close();
}
}
子类TemplateA
package com.blb.demo8;
/**
* 物品1:大象
* @author admin
*
*/
public class TemplateA extends TemplateClass{
@Override
protected void put() {
System.out.println("大象先切块,再放入冰箱");
}
}
子类TemplateB
package com.blb.demo8;
/**
* 物品2:水
* @author admin
*
*/
public class TemplateB extends TemplateClass {
@Override
protected void put() {
System.out.println("先用瓶子将水装起来,再放入冰箱");
}
}
测试类
package com.blb.demo8;
public class Demo1 {
public static void main(String[] args) {
//数据类型 变量名 = new 数据类型();
TemplateClass templateClassA = new TemplateA();
templateClassA.execute();
TemplateClass templateClassB = new TemplateB();
templateClassB.execute();
}
}
通过上面的例子,可以看出:不同的实现类,重写的抽象方法的逻辑不同,导致算法执行的结果也不相同,但是算法骨架是没有改变的。
多态
多态是继封装、继承之后,面向对象的第三大特性。
什么是多态?
同一行为,不同的事物具有不同的表现形式。
多态的分类
- 编译时多态:重载
- 运行时多态:在运行时,同一形为,可以有不同的表现形态
运行时多态
运行时多态的三个条件。
- 重写
- 父类引用指向子类对象(向上转型)
- 继承
多态体现的格式:
父类类型 变量名 = new 子类对象;
变量名.方法名();
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
代码如下:
Fu f = new Zi();
f.method();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后的方法。
父类:
public abstract class Animal {
public abstract void eat();
}
子类:
public class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
public class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
测试类:
public class MyTest1 {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a1 = new Cat();
// 调用的是 Cat 的eat
a1.eat();
// 多态形式,创建对象
Animal a2 = new Dog();
// 调用的是 Dog 的 eat
a2.eat();
}
}
多态的好处
使用多态可以使程序具有更好的扩展性。
多态时成员属性的特性
- 成员变量:编译时看左边(父类),运行时看左边(父类)
- 实例方法:编译时看左边(父类),运行时看右边(子类)
- 静态方法:编译时看左边(父类),运行时看左边(父类)
引用数据类型转换
- 向上转型
子类向父类转换,实现类向接口转换。多态就是向上转型
父类 父类变量名 = new 子类();
Animal a = new Cat();
- 向下转型
子类 子类名称 = (子类)父类;
内部类
内部类就是类中类。
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
成员内部类
public class Outer {
//成员内部类
public class Inner{
}
}
创建
成员内部类要创建对象,必须先创建外部类对象,再通过外部类对象创建成员内部类。
Outer outer = new Outer();
inner inner = outer.new inner();
访问
成员内部类可以访问外部类所有属性和方法
静态内部类
public class Outer {
//静态内部类
public static class Inner{
}
}
创建
//方式一
Outer.Inner inner = new Outer.Inner();
//方式二
import com.blb.demo8.Outer.Inner;
Inner inner = new Inner();
访问
静态内部类可以访问外部类所有静态的属性和方法。
局部内部类
局部内部类:局部内部类是指在一个方法中局部位置定义的内部类。
public class Outer {
public void method() {
class Inner {
// 局部内部类
}
}
}
访问特点:
-
局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰;
-
局部内部类只在当前方法中有效;
-
局部内部类中不能定义 static 成员。
匿名内部类
匿名内部类:是内部类的简化写法,它的本质是一个 带具体实现的 父类或者父接口的 匿名的 子类对象。
开发中,最常用到的内部类就是匿名内部类了。以接口举例,当你使用一个接口时,似乎得做如下几步操作,
定义子类
重写接口中的方法
创建子类对象
调用重写后的方法
我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。
前提:存在一个类或者接口,这里的类可以是具体类也可以是抽象类。
格式:
new 父类名或者接口名(){
// 方法重写
@Override
public void 方法名() {
// 执行语句
}
};
Java内存及垃圾回收机制
程序计数器
程序计数器有以下 三个特点:
- 较小
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所指向的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要指向字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 线程私有
由于 Java 虚拟机的多线程是通过线程轮流 切换并分配处理器指向时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 无异常
如果线程正在执行的是一个 Java 的方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有任何 OutOfMemoryError 情况的区域。
虚拟机栈
- 线程私有
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。
- 描述 Java 方法执行的内存模型
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame,栈帧是方法运行期的基础数据结构)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈栈从入栈到出栈的过程。
- 异常
在 Java 虚拟中,对虚拟机规定了下面两种异常:
① StockOverflowError
当执行 Java 方法是会进行压栈的操作,在栈栈会保存局部变量、操作栈和方法出口等信息。
JVM 规定了栈的最大胜读,如果线程请求执行方法时栈的深度大于规定的深度,就会抛出栈溢出异常 StockOverflowError。
② OutOfMemoryError
如果虚拟机在扩展时无法申请到足够的内存,就会抛出内存溢出异常 OutOfMemoryError。
本地方法栈
本地方法栈的作用于虚拟机非常相似,它有下面两个特点。
① 为 native 服务
本地方法栈与虚拟机栈的区别是虚拟机栈为 Java 服务,而本地方法栈为 native 方法服务。
② 异常
与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java堆
Java 堆(Java Heap)也就是实例堆,它有以下四个特点:
① 最大
对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。
② 线程共享
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
③ 存放实例
此内存区域的唯一目的就是存放对象的实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发送,所有的对象都分配在堆上也渐渐变得不那么“绝对”了 。
④ GC
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”(Garbage Collected Heap)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法(详见下节),所以 Java 堆中还可以细分为:新生代和老年代。如果从内存分配的角度看,线程共享的 Java 堆中可能划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都依然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
方法区
方法区存储的是已经被虚拟机加载的数据,它有以下三个特点:
① 线程共享
方法区域 Java 堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的等数据。
② 存储的数据类型
◇ 类的信息;
◇ 常量;
◇ 静态变量;
◇ 即时编译器编译后的代码,等。
③ 异常
方法区的大小决定履历系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样抛出内存溢出异常 OutOfMemoryError。
方法区又可以分为运行时常量池和直接内存两部分:
① 运行常量池
运行时常量池(Run-time Constant Pool)是方法区的一部分。
Class 文件中处了有类的 版本、字段、方法和接口等描述信息,还有一项信息就是常量池(Constant Pool Table)。
常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池受到方法区内存的限制,当常量池无法再申请到内存时就会抛出 OutOfMemoryError 异常。
② 直接内存
直接内存(Direct Memory)有以下四个特点:
a)在虚拟机数据区外
直接内存不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
b)直接分配
在 JDK1.4 中新加入的 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用操作,这样能避免在 Java 堆和 native 堆中来回复制数据。
c)受设备内存大小限制
直接内存的分配不会受到 Java 堆大小的限制,但是会受到设备总内存(RAM 以及 SWAP 区)大小以及处理器寻址空间的限制。
d)异常
直接内存的容量默认与 Java 对的最大值一样,如果超额申请内存,也可能导致 OOM 异常出现。
什么是垃圾?如何判断一个对象是不是垃圾
- 引用计数法
- 可达性算法
4.2 java垃圾回收机制
java 语言中一个显著的特点就是引入了java回收机制,是c++程序员最头疼的内存管理的问题迎刃而解,它使得java程序员在编写程序的时候不在考虑内存管理。由于有个垃圾回收机制,java中的额对象不在有“作用域”的概念,只有对象的引用才有“作用域”。
垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存;java语言规范没有明确的说明JVM 使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做两件基本事情:(1)发现无用的信息对象;(2)回收将无用对象占用的内存空间。使该空间可被程序再次使用。
标记-清除算法
标记-清除算法(Mark-Sweep)相当于是先把货架上有人买的、没人买的、空着的商品和位置都记录下来,然后再把没人买的商品统一进行下架,这是垃圾收集器中的早期策略。
-
工作原理
◇ 第一步:标记所有需要回收的对象;
◇ 第二步:标记完成后,统一回收所有被标记的对象。
-
缺点
◇ 效率低
标记和清除的效率都不高。
◇ 内存碎片
标记清除后会产生大量不连续的内存碎片,内存碎片大多会导致当程序需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发 GC。
复制算法
为了解决效率问题,复制(Copying)收集算法出现了。
-
工作原理
复制算法把可用内存按容量划分为大小相同的两块,每次只使用其中的一块。
当使用中的这块内存用完了,就把存活的对象复制到另一块内存上,然后把已使用的空间一次清理掉。
这样每次都是对半个内存区域进行回收,内存分配时也不用考虑内存碎片等复杂问题。
-
优点
复制算法的优点是每次只对半个内存区域进行内存回收,分配内存时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。
-
缺点
◇ 浪费空间
把内存缩小一半用太浪费空间。
◇ 有时效率低
在对象存活率高时,要进行较多的复制操作,这时效率就低了。
标记-整理算法
在复制算法中,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用内存中所有对象都存活的低端情况所以养老区不能用这种算法。
根据养老区的特点,有人提出了一种标记-整理(Mark-Compact)算法。
-
工作原理
标记-整理算法的标记过程与标记-清除算法一样,但后续步骤是让所有存活的对象向一端移动,然后直接清除掉边界外的内存。
分代收集算法
现代商业虚拟机的垃圾回收机制都是采用分代收集算法(Generational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,这样就可以根据各个区域的特点采用最适当的收集算法。
在新生区,每次垃圾收集都有大批对象死去,只有少了存活,所以可以用复制算法。
养老区中因为对象存活率高、没有额外空间对它进行担保,就必须使用标记-清除或标记-整理算法进行回收。
对内存可分为新生区、养老区永久存储区三个区域。
一、新生区(Young Generation Space)
新生区也叫年轻代。
-
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
-
新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。
回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
-
当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
-
新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
二、养老区(Tenure Generation Space)
养老区也叫老年代。
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
三、永久存储区(Permanent Space)
永久存储区也叫持久代。
持久代用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。