设计模式面试专题

2021-03-11  本文已影响0人  莫生人

1.请列举出在JDK中几个常用的设计模式?

单例模式(Singleton pattern)用于Runtime,Calendar和其他的一些类中。工厂模式(Factory pattern)被用于各种不可变的类如Boolean,像Boolean.valueOf,观察者模式(Observer pattern)被用于Swing和很多的事件监听中。装饰器设计模式(Decorator design pattern)被用于多个Java IO类中。

2.什么是设计模式?你是否在你的代码里面使用过任何设计模式?

设计模式是世界上各种各样程序员用来解决特定设计问题的尝试和测试的方法。设计模式是代码可用性的延伸。

3.Java中什么叫单例设计模式?请用Java写出线程安全的单例模式

单例模式重点在于在整个系统上共享一些创建时较耗资源的对象。整个应用中只维护一个特定类实例,它被所有组件共同使用。Java.lang.Runtime是单例模式的经典例子。从Java5开始你可以使用枚举(enum)来实现线程安全的单例。

使用单例模式需要注意的关键点

1.将构造函数访问修饰符设置为private
2.通过一个静态方法或者枚举返回单例类对象
3.确保单例类的对象有且只有一个,特别是在多线程环境下
4.确保单例类对象在反序列化时不会重新构建对象

1).饿汉式(线程安全,调用效率高,但是不能延时加载)

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    private static Singleton getInstance() {
        return instance;
    }
}

2).懒汉式(线程安全,调用效率不高,但是能延时加载;每次调用都会同步,消耗不必要的资源,一般不建议使用)

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    private static synchronized Singleton getInstance() {
        if(instance == null) {
                instance = new Singleton();
        }
        return instance;
    }
}

3).DCL(Double CheckLock)实现单例(双重锁判断机制,资源利用率高,既能够在需要的时候才初始化实例,又能保证线程安全,同时调用getInstance()方法不进行同步锁,效率高;由于JVM底层模型原因,偶尔会出问题,不建议使用)

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

4).静态内部类(第一次加载Singleton类时不会初始化instance,只有在第一次调用getInstance()方法时,虚拟机才会加载SingletonHolder类,初始化instance;既保证线程安全,单例对象的唯一,也延迟了单例的初始化,推荐使用这种方式来实现单例模式)

public class Singleton {
    private Singleton() {
    }

    private static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
}

5).枚举单例(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用)

public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {
        System.out.println("do something");
    }
}

6).容器实现单例(可以管理多个单例类型,使用时根据key获取对象对应类型的对象。这种方式可以通过统一的接口获取操作,隐藏了具体实现,降低了耦合度。)

import java.util.HashMap;
import java.util.Map;

/**
 * 容器类实现单例模式
 */
public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String, Object>();

    public static void regsiterService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }
}

4.在Java中,什么叫观察者设计模式(observer design pattern)?

观察者模式是基于对象的状态变化和观察者的通讯,以便他们作出相应的操作。简单的例子就是一个天气系统,当天气变化时必须在展示给公众的视图中进行反映。这个视图对象是一个主体,而不同的视图是观察者。

1).主题Subject

首先定义一个观察者数组,并实现增、删及通知操作。它的职责很简单,就是定义谁能观察,谁不能观察,用Vector是线程同步的,比较安全,也可以使用ArrayList,是线程异步的,但不安全。

public class Subject {
    //观察者数组
    private Vector<Observer> oVector = new Vector<>();
    
    //增加一个观察者
    public void addObserver(Observer observer) {
        this.oVector.add(observer);
    }

    //删除一个观察者
    public void deleteObserver(Observer observer) {
        this.oVector.remove(observer);
    }

    //通知所有观察者
    public void notifyObserver() {
        for(Observer observer : this.oVector) {
            observer.update();
        }
    }
}

2).抽象观察者Observer

观察者一般是一个接口,每一个实现该接口的实现类都是具体观察者。

public interface Observer {
    //更新
    public void update();
}

3).具体主题

继承Subject类,在这里实现具体业务,在具体项目中,该类会有很多变种。

public class ConcreteSubject extends Subject {
    //具体业务
    public void doSomething() {
        super.notifyObserver();
    }
}

4).具体观察者

实现Observer接口。

public class ConcreteObserver implements Observer {
    @Override
    public void update() {
        System.out.println("收到消息,进行处理");
    }
}

5).Client客户端

首先创建一个被观察者,然后定义一个观察者,将该被观察者添加到该观察者的观察者数组中,进行测试。

public class Client {
    public static void main(String[] args) {
        //创建一个主题
        ConcreteSubject subject = new ConcreteSubject();
        //定义一个观察者
        Observer observer = new ConcreteObserver();
        //观察
        subject.addObserver(observer);
        //开始活动
        subject.doSomething();
    }
}

5.使用工厂模式最主要的好处是什么?在哪里使用?

工厂模式的最大好处是增加了创建对象时的封装层次。如果你使用工厂来创建对象,之后你可以使用更高级和更高性能的实现来替换原始的产品实现或类,这不需要在调用层做任何修改。

三种不同的工程模式

1.简单工程模式:该模式对对象创建管理方式最为简单,因为其仅仅简单的对不同类对象的创建进行了一层薄薄的封装。该模式通过向工厂传递类型来指定要创建的对象。缺点是破坏了开放,封闭原则。其UML类图如下:


image.png

Phone类:手机标准规范类(AbstractProduct)

public interface Phone {
    void make();
}

XiaoMiPhone类:制造小米手机(Product1)

public class XiaoMiPhone implements Phone {
    public XiaoMiPhone() {
        this.make();
    }

    @Override
    public void make() {
        System.out.println("make xiaomi phone!");
    }
}

IPhone类:制造苹果手机(Product2)

public class IPhone implements Phone {
    public IPhone() {
        this.make();
    }

    @Override
    public void make() {
        System.out.println("make iphone!");
    }
}

PhoneFactory类:手机代工厂(Factory)

public class PhoneFactory {
    public Phone makePhone(String phoneType) {
        if(phoneType.equalsIgnoreCase("MiPhone")){
            return new MiPhone();
        } else if(phoneType.equalsIgnoreCase("iPhone")) {
            return new IPhone();
        }
        return null;
    }
}

2.工厂方法模式(Factory Method):和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式将生成具体产品的任务分发给具体的产品工厂,也就是定义一个抽象工厂,其定义了产品的生产接口,但不负责具体的产品,将生产任务交给不同的派生类工厂。这样不用通过指定类型来创建对象了。对简单工厂做了相应的改进,改正了简单工厂破坏开放封闭原则的错误。其UML类图如下:


image.png

AbstractFactory类:生产不同产品的工厂的抽象类

public interface AbstractFactory {
    Phone makePhone();
}

XiaoMiFactory类:生产小米手机的工厂(ConcreteFactory1)

public class XiaoMiFactory implements AbstractFactory{
    @Override
    public Phone makePhone() {
        return new XiaoMiPhone();
    }
}

AppleFactory类:生产苹果手机的工厂(ConcreteFactory2)

public class AppleFactory implements AbstractFactory {
    @Override
    public Phone makePhone() {
        return new IPhone();
    }
}
  1. 抽象工厂模式(Abstract Factory):上面两种模式不管工厂怎么拆分抽象,都只是针对一类产品Phone(AbstractProduct),如果要生成另一种产品PC,应该怎么表示呢?最简单的方式是把2中介绍的工厂方法模式完全复制一份,不过这次生产的是PC。但同时也就意味着我们要完全复制和修改Phone生产管理的所有代码,显然这是一个笨办法,并不利于扩展和维护。抽象工厂模式通过在AbstarctFactory中增加创建产品的接口,并在具体子工厂中实现新加产品的创建,当然前提是子工厂支持生产该产品。否则继承的这个接口可以什么也不干。其UML类图如下:


    image.png

    为了弄清楚上面的结构,我们使用具体的产品和工厂来表示上面的UML类图,能更加清晰的看出模式是如何演变的:


    image.png
    PC类:定义PC产品的接口(AbstractPC)
public interface PC {
    void make();
}

XiaoMiPC类:定义小米电脑产品(XiaoMiPC)

public class XiaoMiPC implements PC {
    public XiaoMiPC () {
        this.make();
    }

    @Override
    public void make() {
        System.out.println("make xiaomi PC!");
    }
}

MAC类:定义苹果电脑产品(MAC)

public class MAC implements PC {
    public MAC() {
        this.make();
    }

    @Override
    public void make() {
        System.out.println("make MAC!");
    }
}

下面需要修改工厂相关的类的定义:
AbstractFactory类:增加PC产品制造接口

public interface AbstractFactory {
    Phone makePhone();
    PC makePC();
}

XiaoMiFactory类:增加小米PC的制造(ConcreteFactory1)

public class XiaoMiFactory implements AbstractFactory{
    @Override
    public Phone makePhone() {
        return new XiaoMiPhone();
    }

    @Override
    public PC makePC() {
        return new XiaoMiPC();
    }
}

AppleFactory类:增加苹果PC的制造(ConcreteFactory2)

public class AppleFactory implements AbstractFactory {
    @Override
    public Phone makePhone() {
        return new IPhone();
    }

    @Override
    public PC makePC() {
        return new MAC();
    }
}

总结:

上面介绍的三种工厂模式有各自的应用场景,实际应用时能解决问题满足需求即可,可灵活变通,无所谓高级与低级。此外无论哪种模式,由于可能封装了大量对象和工厂创建,新加产品需要修改已定义好的工厂相关的类,因此对于产品和工厂的扩展不太友好,利弊需要权衡一下。

6.举一个用Java实现的装饰模式(decorator design pattern)?它是作用于对象层次还是类层次?

装饰模式增强了单个对象的能力。Java IO到处都使用了装饰模式,典型例子就是Buffered系列类如BufferedReader和BufferedWriter,它们增强了Reader和Writer对象,以实现提升性能的Buffer层次的读取和写入。

什么是装饰模式

动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更灵活。UML结构图如下:


image.png

其中,Component是抽象构件,定义一个对象接口,可以给这些对象动态地添加职责;ConreteComponent定义一个具体对象,也可以给这个对象添加一些职责;Decorator是装饰抽象类,实现接口或抽象方法;ConreteDecorator是具体装饰对象,起到给Component添加职责的功能。
1.Component抽象类:是一个接口或是抽象类,就是定义我们最核心的对象,也就是最原始的对象。

public abstract class Component {
    public abstract void operation();
}
  1. ConretetComponent类:具体构件,通过继承实现Component抽象类中的抽象方法。是最核心、最原始、最基本的接口或抽象类的实现,我们要装饰的就是它。
public class ConcreteComponent extends Component {

    @Override
    public void operation() {
        System.out.println("具体对象的操作");
    }
}
  1. Decorator装饰类:一般是一个抽象类,在其属性里必然有一个private变量指向Component抽象构件。
public abstract class Decorator extends Component {

    private Component component = null;

    //通过构造函数传递给被修饰者
    public Decorator(Component component) {
        this.component = component;
    }

    //委托给被修饰者执行
    @Override
    public void operation() {
        if(component != null) {
            this.component.operation();
        }
    }

}
  1. ConcreteDecorator类:我们可以写多个具体实现类,把最核心的、最原始的、最基本的东西装饰成其它东西。
    这里就写两个类,稍改一下二者的实现顺序,看看结果。A类,它的operation()方法先执行了method1()方法,再执行了Decorator的operation()方法。
public class ConcreteDecoratorA extends Decorator {

    //定义被修饰者
    public ConcreteDecoratorA(Component component) {
        super(component);
    }

    //定义自己的修饰方法
    private void method1() {
        System.out.println("method1 修饰");
    }

    @Override
    public void operation() {
        this.method1();
        super.operation();
    }
}

B类,它的operation()方法先执行了Decorator的operation()方法,再执行了method2()方法。

public class ConcreteDecoratorB extends Decorator {

    //定义被修饰者
    public ConcreteDecoratorB(Component component) {
        super(component);
    }

    //定义自己的修饰方法
    private void method2() {
        System.out.println("method2 修饰");
    }

    @Override
    public void operation() {
        super.operation();
        this.method2();
    }
}
  1. Client客户端
public class Client {

    public static void main(String[] args) {
        Component component = new ConcreteComponent();
        //第一次修饰
        component = new ConcreteDecoratorA(component);
        //第二次修饰
        component = new ConcreteDecoratorB(component);
        //修饰后运行
        component.operation();
    }
}

6.运行结果

method1 修饰
具体对象的操作
method2 修饰

如果我们将B的运算顺序改为与A相同的,即先this再super,运行结果如下:

method2 修饰
method1 修饰
具体对象的操作

所以我们可以知道,原始方法和装饰方法的执行顺序在具体的装饰类是固定的,可以通过方法重载实现多种执行顺序。至于上面的具体对象操作为什么只输出了一次,因为在装饰者类中,我们有一个“component != null“的判断条件,控制了对象的引用,更多类似的内容可参考[单例模式]。

7.在Java 中,为什么不允许从静态方法中访问非静态变量?

Java 中不能从静态上下文访问非静态数据只是因为非静态变量是跟具体的对象实例关联的,而静态的却没有和任何实例关联。

8.设计一个ATM 机,请说出你的设计思路?

比如设计金融系统来说,必须知道它们应该在任何情况下都能够正常工作。不管是断电还是其他情况,ATM 应该保持正确的状态(事务), 想想加锁(locking)、事务(transaction)、错误条件(error condition)、边界条件(boundary condition)等等。尽管你不能想到具体的设计,但如果你可以指出非功能性需求,提出一些问题,想到关于边界条件,这些都会是很好的。

9.在Java 中,什么时候用重载,什么时候用重写?

如果你看到一个类的不同实现有着不同的方式来做同一件事,那么就应该用重写(overriding),而重载(overloading)是用不同的输入做同一件事。在Java 中,重载的方法签名不同,而重写并不是。

10.举例说明什么情况下会更倾向于使用抽象类而不是接口?

接口和抽象类都遵循”面向接口而不是实现编码”设计原则,它可以增加代码的灵活性,可以适应不断变化的需求。下面有几个点可以帮助你回答这个问题:在Java 中,你只能继承一个类,但可以实现多个接口。所以一旦你继承了一个类,你就失去了继承其他类的机会了。接口通常被用来表示附属描述或行为如:Runnable、Clonable、Serializable等等,因此当你使用抽象类来表示行为时,你的类就不能同时是Runnable和Clonable(注:这里的意思是指如果把Runnable等实现为抽象类的情况),因为在Java 中你不能继承两个类,但当你使用接口时,你的类就可以同时拥有多个不同的行为。在一些对时间要求比较高的应用中,倾向于使用抽象类,它会比接口稍快一点。如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码,那么抽象类是一个更好的选择。有时,接口和抽象类可以一起使用,接口中定义函数,而在抽象类中定义默认的实现。

上一篇下一篇

猜你喜欢

热点阅读