Java研发工程师的面试宝典

Java基础

面向对象和面向过程的区别?

面向过程

  • 定义:
  • 优点:性能比面向对象高,因为类调用时需要实例化
  • 缺点:没有面向对象易维护,易复用,易扩展

面向对象

  • 定义:面向对象程序设计是种具有对象概念的程序编程范型,同时也是一种程序开发的抽象方针。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

Java的四个基本特性?

  • 抽象:就是把现实生活中的某一类东西提取出来,用程序代码进行表示,通常叫做类或者接口。抽象包括于两个方面:一个是数据抽象,一个是过程抽象。数据抽象也就是对象的属性,过程抽象是对象的行为特征。
  • 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行隐藏。封装也分为属性的封装和方法的封装。
  • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。
  • 多态:允许不同的子类对象对同一消息做出响应。即父类引用指向子类对象。

重载(Overload)和重写(Override)的区别?

  • 重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
  • 重写:发生在父子继承关系中,方法名和参数列表必须相同,返回值可以小于等于父类定义的返回值,抛出异常小于等于父类,访问修饰符大于等于父类。

构造器Constructor是否可被重写

不可以。构造器不能被重写,且不能用static修饰,只能添加private、protected、public在三个权限修饰符,且不能有返回语句。

访问控制符private、default、protected、public的区别

  • private修饰的方法和属性只能在本类中被访问
  • default修饰的方法和属性在同包下可访问
  • protected修饰的方法和属性在同包和子类中可访问
  • public修饰的方法和属性是公开的

是否可以继承String类?为什么?

不能。String类是final关键字修饰的,所以不可继承。

String、StringBuilder、StringBuffer的区别?

  • 可变性:
    • 不可变对象:String
    • 可变对象:StringBuilder、StringBuffer
  • 线程安全性:
    • 线程安全的:StringBuffer
    • 线程不安全:StringBuilder

hashCode()和equals()的关系

如果两个对象equals()的返回值为true,那么他们应该有同样的hashCode值。反之,若两个对象由相同的hashCode他们可能不相等(equals()返回false)。

抽象类和接口的区别

  • 语法层次:抽象类和接口的定义方式有区别
  • 抽象层次:抽象类是对对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
  • 跨域不同:抽象类所体现的是一种继承关系,想要使得继承关系合理,父类和子类之间必须存在“is-a”的关系,即父类和子类在概念本质上应该是相同的。对于接口来说,并不要求接口的实现者和接口定义在概念本质上是一致的,仅仅是实现了接口定义的契约而已,表达“like-a”关系。

Java中的基本类型?自动装箱与拆箱?

  • Java中的基本类型有8种:byte、short、int、long、float、double、boolean、char;
  • 对应的包装类类别:Byte、Short、Integer、Long、Float、Double、Boolean、Character
  • 装箱:将基本应用类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本类型;
  • Java使用自动装箱和拆箱机制,节省了常用数值的内存开销和创建对象的开销,提升了效率。由编译器来完成,编译器在编译期间根据语法决定是否进行装箱和拆箱操作。

什么是泛型?为什么要使用泛型?什么是类型擦除?

  • 泛型即参数化类型。创建集合时就指定集合元素的类型,确保该集合只能保存其指定的类型元素,避免使用强制类型转换。
  • Java编译器生成的字节码是不包含泛型信息的,泛型类型信息将在编译处理时被擦除,这个过程被称为类型擦除。类型擦除的主要过程如下:
    1. 将所有的泛型参数用其最左边(最顶级的父类型)类型替换。
    2. 移除所有的泛型信息。

Java中的集合类及关系图

  • List和Set继承自Collection接口
    • Set无序、不允许重复元素。HashSet和TreeSet是两个主要的实现类;
    • List有序,运行重复元素。ArrayList和LinkedList是两个主要的实现类;
  • Map也属于集合系统,但是没有实现Collection接口。Map主要表示Key对Value的映射集合。Key值不可重复,但是Vaule可以重复。HashMap和TreeMap是两个主要的实现类。

HashMap的实现原理?

待填坑

Hashtable的实现原理?

待填坑

HashMap和Hashtable的区别?

Hashtable如何实现线程安全?

ArrayList和Vector的区别?

ArrayList和LinkedList的区别和使用场景?

区别

  • ArrayList基于数组实现,可以简单的认为ArrayList是一个大小可变的数组,随着越来越多的元素被添加到ArrayList中,其规模动态增加。
  • LinkedList基于双向链表实现,相较于ArrayList,元素增删的速度较快,但是随机访问的速度较慢,且LinkedList实现了Queue接口,还可以作为队列使用。

使用场景

  • LinkedList适用于频繁增删的场景
  • ArrayList适用于检索频繁的场景

Collection和Collections的区别?

  • java.util.Collection是一个集合接口。他提供了对集合对象进行基本操作的通用接口方法,在Java类库中很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
  • java.util.Collections是一个包装类。它包含各种有关机和操作的静态方法,此类不能实例化,就像一个工作类,服务于Java的集合框架

ConcurrentHashMap的实现原理

Java的异常机制?Error、Exception的区别?

在Java中,所有的异常都有一个共同的祖先Throwable。Throwable指定代码中可用异常传播机制通过Java应用程序传输的任何问题的共性。Throwable有两个重要的子类:

  • Error:一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、内存空间不足、方法调用栈溢出。对于这类错误导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误建议程序终止。
  • Exception:表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能的处理异常,使程序恢复运行,而不会随意终止。

Unchecked Exception和Checked Exception?

  • Unchecked Exception
    • 指的是程序的瑕疵或逻辑错误,并在运行时无法恢复
    • 包括Error和RuntimeException及其子类
    • 语法上不需要声明抛出异常
  • Checked Exception
    • 代表程序不能直接控制的无效界外情况(如用户输入、数据库问题、网络异常、文件丢失等)
    • 除了Error和RuntimeException及其子类之外
    • 需要使用try-catch处理或throws声明抛出异常

JavaEE

Servlet生命周期和各个方法?

Servlet的生命周期

Servlet的生命周期分为四个部分,分别是:加载->实例化->服务->销毁

主要方法

  • init():在Servlet的生命周期中,仅执行一次init()方法。它是在服务器装入Servlet时执行,负责初始化Servlet对象。可以通过配置服务器配置,以在服务器启动或在客户机首次访问Servlet时装入Servlet对象。无论有多少客户机访问Servlet,都不会重复执行init()方法。
  • service():Servlet的核心方法,负责响应客户的请求。每当一个客户请求一个HttpServlet对象,该对象的service()方法就要调用,而且传递给这个方法一个ServletRequest对象和一个ServletResponse对象作为参数。
  • destory():仅执行一次,在服务器端停止且卸载Servlet时执行该方法。当Servlet对象退出生命周期时,负责释放占用的资源。一个Servlet在运行service()方法时可能会产生其它的线程,因此需要确认在调用destory()方法时,这些线程已经终止或者完成。

Servlet中如何定义Filter?

2017/8/26 posted in  Java
 

设计模式-适配器模式

将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

与电源适配器相似,在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。
适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。

角色

在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。在实际开发中,对象适配器的使用频率更高,对象适配器模式结构如图所示:

在对象适配器模式结构图中包含如下几个角色:

  • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
  • Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
  • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

实现

根据对象适配器模式结构图,在对象适配器中,客户端需要调用request()方法,而适配者类Adaptee没有该方法,但是它所提供的specilRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request()方法中调用适配者的specilRequest()方法。因为适配器类与适配者类是关联关系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。典型的对象适配器代码如下所示:

public class Adapter implements Target {

    // 适配者
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.specilRequest();
    }

}

类适配器

除了对象适配器模式之外,适配器模式还有一种形式,那就是类适配器模式,类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系,类适配器模式结构如图所示:

根据类适配器模式结构图,适配器类实现了抽象目标类接口Target,并继承了适配者类,在适配器类的request()方法中调用所继承的适配者类的specificRequest()方法,实现了适配。

class Adapter extends Adaptee implements Target {  
    public void request() {  
        specificRequest();  
    }  
}  

由于Java、C#等语言不支持多重类继承,因此类适配器的使用受到很多限制,例如如果目标抽象类Target不是接口,而是一个类,就无法使用类适配器;此外,如果适配者Adapter为最终(Final)类,也无法使用类适配器。在Java等面向对象编程语言中,大部分情况下我们使用的是对象适配器,类适配器较少使用。

双向适配器

在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器,其结构示意图如图所示:

class Adapter implements Target,Adaptee {  
    //同时维持对抽象目标类和适配者的引用  
    private Target target;  
    private Adaptee adaptee;  
      
    public Adapter(Target target) {  
        this.target = target;  
    }  
      
    public Adapter(Adaptee adaptee) {  
        this.adaptee = adaptee;  
    }  
      
    public void request() {  
        adaptee.specificRequest();  
    }  
      
    public void specificRequest() {  
        target.request();  
    }  
}  

总结

适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用,在spring等开源框架、驱动程序设计(如JDBC中的数据库驱动程序)中也使用了适配器模式。

主要优点

无论是对象适配器模式还是类适配器模式都具有如下优点:

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  2. 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  3. 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

具体来说,类适配器模式还有如下优点:

  1. 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

对象适配器模式还有如下优点:

  1. 一个对象适配器可以把多个不同的适配者适配到同一个目标;
  2. 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。

主要缺点

类适配器模式的缺点如下:

  1. 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
  2. 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类;
  3. 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

对象适配器模式的缺点如下:

  1. 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

适用场景

在以下情况下可以考虑使用适配器模式:

  1. 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
  2. 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

实现

相关代码Github地址
文章参考-刘伟

2017/3/16 posted in  Java
 

设计模式-命令模式

将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式为此类问题提供了一个较为完美的解决方案。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。

命令模式的定义比较复杂,提到了很多术语,例如“用不同的请求对客户进行参数化”、“对请求排队”,“记录请求日志”、“支持可撤销操作”等,在后面我们将对这些术语进行一一讲解。
命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如图所示:

角色

在命令模式结构图中包含如下几个角色:

  • Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
  • ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
  • Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
  • Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。

命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。

实现

典型的抽象命令类代码如下所示:

public abstract class Command {
    public abstract void execute();
}

对于请求发送者即调用者而言,将针对抽象命令类进行编程,可以通过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法,其典型代码如下所示:

public class Invoker {
    private Command command;

    public Invoker(Command command) {
        this.command = command;
    }

    public void setCommand(Command command) {
        this.command = command;
    }

    public void call() {
        this.command.execute();
    }
}

具体命令类继承了抽象命令类,它与请求接收者相关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action(),其典型代码如下所示:

public class ConcreteCommand extends Command {
    /**
     * 命令接收者
     */
    private Receiver receiver;

    public ConcreteCommand(Receiver receiver) {
        this.receiver = receiver;
    }

    public void setReceiver(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.action();
    }

}

请求接收者Receiver类具体实现对请求的业务处理,它提供了action()方法,用于执行与请求相关的操作,其典型代码如下所示:

public class Receiver {
    public void action() {
        System.out.println("做点有用的事情");
    }
}

命令队列

有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者,CommandQueue类的典型代码如下所示:

public class CommandQueue {
    private ArrayList<Command> commands = new ArrayList<Command>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    public void removeCommand(Command command) {
        commands.remove(command);
    }

    // 循环调用每一个命令对象的execute()方法
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }
}

在增加了命令队列类CommandQueue以后,请求发送者类Invoker将针对CommandQueue编程,代码修改如下:

public class CommandQueueInvoker {
    private CommandQueue commandQueue; // 维持一个CommandQueue对象的引用

    // 构造注入
    public CommandQueueInvoker(CommandQueue commandQueue) {
        this.commandQueue = commandQueue;
    }

    // 设值注入
    public void setCommandQueue(CommandQueue commandQueue) {
        this.commandQueue = commandQueue;
    }

    // 调用CommandQueue类的execute()方法
    public void call() {
        commandQueue.execute();
    }
}

命令队列与我们常说的“批处理”有点类似。批处理,顾名思义,可以对一组对象(命令)进行批量处理,当一个发送者发送请求后,将有一系列接收者对请求作出响应,命令队列可以用于设计批处理应用程序,如果请求接收者的接收次序没有严格的先后次序,我们还可以使用多线程技术来并发调用命令对象的execute()方法,从而提高程序的执行效率。

撤销

在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。
除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现。

总结

命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于GUI的软件开发,无论是在电脑桌面应用还是在移动应用中,命令模式都得到了广泛的应用。

主要优点

命令模式的主要优点如下:

  1. 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
  2. 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
  3. 可以比较容易地设计一个命令队列或宏命令(组合命令)。
  4. 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。

主要缺点

命令模式的主要缺点如下:

使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

适用场景

在以下情况下可以考虑使用命令模式:

  1. 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
  2. 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
  3. 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
  4. 系统需要将一组操作组合在一起形成宏命令。

实现

相关代码Github地址
文章参考-刘伟

2017/3/14 posted in  Java
 

设计模式-单例模式

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。单例模式结构如图所示:

单例模式结构图中只包含一个单例角色:

  • Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。

实现

饿汉单例类

饿汉式单例类是实现起来最简单的单例类,饿汉式单例类结构图如图所示:

从图中可以看出,由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,代码如下所示:

public class EagerSingleton {
    private static final EagerSingleton EAGER_SINGLETON = new EagerSingleton();

    private EagerSingleton() {

    }

    public static EagerSingleton getInstance() {
        return EAGER_SINGLETON;
    }
}

懒汉单例模式

除了饿汉式单例,还有一种经典的懒汉式单例。懒汉式单例类结构图如图所示:

从图中可以看出,懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized,代码如下所示:

public class LazySingleton1 {
    private static LazySingleton1 instance = null;

    private LazySingleton1() {
    }

    synchronized public static LazySingleton1 getInstance() {
        if (instance == null) {
            instance = new LazySingleton1();
        }
        return instance;
    }
}

该懒汉式单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。但是,上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢?我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码instance = new LazySingleton();进行锁定即可。因此getInstance()方法可以进行如下改进:

public static LazySingleton getInstance() {   
    if (instance == null) {  
        synchronized (LazySingleton.class) {  
            instance = new LazySingleton();   
        }  
    }  
    return instance;   
} 

问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。原因如下:假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:

class LazySingleton {   
    private volatile static LazySingleton instance = null;   
  
    private LazySingleton() { }   
  
    public static LazySingleton getInstance() {   
        //第一重判断  
        if (instance == null) {  
            //锁定代码块  
            synchronized (LazySingleton.class) {  
                //第二重判断  
                if (instance == null) {  
                    instance = new LazySingleton(); //创建单例实例  
                }  
            }  
        }  
        return instance;   
    }  
}  

需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。

懒汉和饿汉的比较

饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

静态内部类实现法

饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。
在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:

class Singleton {  
    private Singleton() {  
    }  
      
    private static class HolderClass {  
            private final static Singleton instance = new Singleton();  
    }  
      
    public static Singleton getInstance() {  
        return HolderClass.instance;  
    }  
      
    public static void main(String args[]) {  
        Singleton s1, s2;   
            s1 = Singleton.getInstance();  
        s2 = Singleton.getInstance();  
        System.out.println(s1==s2);  
    }  
}  

编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。

总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。

主要优点

单例模式的主要优点如下:

  1. 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  2. 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  3. 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

主要缺点

单例模式的主要缺点如下:

  1. 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

适用场景

在以下情况下可以考虑使用单例模式:

  1. 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  2. 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

实现

相关代码Github地址
文章参考-刘伟

2017/3/13 posted in  Java
 

设计模式-抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。

抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。
在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族。

角色

在抽象工厂模式结构图中包含如下几个角色:

  1. AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
  2. ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
  3. AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
  4. ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

抽象工厂模式结构如图所示:

实现

在抽象工厂中声明了多个工厂方法,用于创建不同类型的产品,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示:

public abstract class AbstractFactory {
    /**
     * 生产A产品的具体实例
     * 
     * @return
     */
    public abstract ProductA createProductA();

    /**
     * 生产B产品的具体实例
     * 
     * @return
     */
    public abstract ProductB createProductB();
}

具体工厂实现了抽象工厂,每一个具体的工厂方法可以返回一个特定的产品对象,而同一个具体工厂所创建的产品对象构成了一个产品族。对于每一个具体工厂类,其典型代码如下所示:

public class ConcreteFactoryOne extends AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB();
    }
}

public class ConcreteFactoryTwo extends AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA2();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB2();
    }
}

总结

抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。

主要优点

抽象工厂模式的主要优点如下:

  1. 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
  2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
  3. 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。

主要缺点

增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。

适用场景

在以下情况下可以考虑使用抽象工厂模式:

  1. 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。
  2. 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
  3. 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型。
  4. 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。

实现

相关代码Github地址
文章参考-刘伟

2017/3/12 posted in  Java