Java面试题--设计模式

一、Java 中有几种设计模式?

Java 中一般认为有 23 种设计模式

分为三大类:

1. 创建型模式 5 种

① 工厂方法模式

② 抽象工厂模式

③ 单例模式

④ 建造者模式

⑤ 原型模式

2. 结构型模式 7 种

① 适配器模式

② 装饰器模式

③ 代理模式

④ 外观模式

⑤ 桥接模式

⑥ 组合模式

⑦ 享元模式

3. 行为型模式 11 种

① 策略模式

② 模板方法模式

③ 观察者模式

④ 迭代子模式

⑤ 责任链模式

⑥ 命令模式

⑦ 备忘录模式

⑧ 状态模式

⑨ 访问者模式

⑩ 中介者模式

⑪ 解释器模式

二、什么是单例设计模式?

1. 单例模式定义

单例模式确保某个类只有一个实例,而

且自行实例化并向整个系统提供这个实

在计算机系统中,线程池、缓存、日志

对象、对话框、打印机、显卡的驱动程

序对象常被设计成单例,选择单例模式

就是为了避免不一致状态

2. 单例模式的特点

① 单例类只能有一个实例

② 单例类必须自己创建自己的唯一实例

③ 单例类必须给所有其他对象提供这一

实例

④ 单例模式保证了全局对象的唯一性,

比如系统启动读取配置文件就需要单

例保证配置的一致性

3. 单例的四大原则

① 构造器私有化

② 以静态方法或者枚举返回实例

③ 确保实例只有一个,尤其是多线程

环境

④ 确保反序列化时不会重新构建对象

4. 实现单例模式的方式

(1) 饿汉式 (立即加载):

饿汉式单例在类加载初始化时就创建好

一个静态的对象供外部使用,除非系统

重启,这个对象不会改变,所以本身就

是线程安全的
Singleton 通过将构造方法限定为 private

避免了类在外部被实例化,在同一个虚拟

机范围内,Singleton 的唯一实例只能通

过 getInstance() 方法访问 (事实上,通过

Java 反射机制是能够实例化构造方法为

private 的类的,会使 Java 单例实现失效)

java 复制代码
/**
 * 饿汉式(立即加载)
 */
public class Singleton1 {

    /**
     * 私有构造
     */
    private Singleton1() {
        System.out.println("构造函数Singleton1");
    }

    /**
     * 初始值为实例对象
     */
     private static Singleton1 single = new Singleton1();

     /**
      * 静态工厂方法
      * @return 单例对象
      */
     public static Singleton1 getInstance() {
        System.out.println("getInstance");
        return single;
     }

     public static void main(String[] args){
        System.out.println("初始化");
        Singleton1 instance = Singleton1.getInstance();
     }
}
(2) 懒汉式 (延迟加载):

该示例虽然用延迟加载方式实现了懒汉

式单例,但在多线程环境下会产生多个

Singleton 对象

java 复制代码
/**
 * 懒汉式(延迟加载)
 */
public class Singleton2 {

    /**
     * 私有构造
     */
    private Singleton2() {
        System.out.println("构造函数Singleton2");
    }

    /**
     * 初始值为null
     */
    private static Singleton2 single = null;

    /**
     * 静态工厂方法
     * @return 单例对象
     */
    public static Singleton2 getInstance() {
        if(single == null){
            System.out.println("getInstance");
            single = new Singleton2();
        }
        return single;
    }

    public static void main(String[] args){
        System.out.println("初始化");
        Singleton2 instance = Singleton2.getInstance();
    }
}
(3) 同步锁 (解决线程安全问题):

在方法上加 synchronized 同步锁或是

用同步代码块对类加同步锁,此种方

式虽然解决了多个实例对象问题,但

是该方式运行效率却很低下,下一个

线程想要获取对象,就必须等待上一

个线程释放锁之后,才可以继续运行

java 复制代码
/**
 *
 * 同步锁(解决线程安全问题)
 */
public class Singleton3 {

    /**
     * 私有构造
     */
    private Singleton3() {}

    /**
     * 初始值为null
     */
    private static Singleton3 single = null;
    
    public static Singleton3 getInstance() {
        // 等同于 synchronized public static Singleton3 getInstance()
        synchronized(Singleton3.class){
            // 注意:里面的判断是一定要加的,否则出现线程安全问题
            if(single == null){
                single = new Singleton3();
            }
        }
        return single;
    }
}
(4) 双重检查锁 (提高同步锁的效率):

使用双重检查锁进一步做了优化,可

以避免整个方法被锁,只对需要锁的

代码部分加锁,可以提高执行效率

java 复制代码
/**
 * 双重检查锁(提高同步锁的效率)
 */
public class Singleton4 {

    /**
     * 私有构造
     */
    private Singleton4() {}

    /**
     * 初始值为null
     */
    private static Singleton4 single = null;

    /**
     * 双重检查锁
     * @return 单例对象
     */
    public static Singleton4 getInstance() {
        if (single == null) {
            synchronized (Singleton4.class) {
                if (single == null) {
                    single = new Singleton4();
                }
            }
        }
        return single;
    }
}
(5) 静态内部类:

引入了一个内部静态类 (static class),静

态内部类只有在调用时才会加载,它保证

了 Singleton 实例的延迟初始化,又保证

了实例的唯一性

它把 singleton 的实例化操作放到一个静

态内部类中,在第一次调用 getInstance()

方法时,JVM 才会去加载 InnerObject 类,

同时初始化 singleton 实例,所以能让

getInstance() 方法线程安全

特点:即能延迟加载,也能保证线程安全

静态内部类虽然保证了单例在多线程并发

下的线程安全性,但是在遇到序列化对象

时,默认的方式运行得到的结果就是多例

java 复制代码
/**
 *
 * 静态内部类(延迟加载,线程安全)
 */
public class Singleton5 {
    /**
     * 私有构造
     */
    private Singleton5() {}

    /**
     * 静态内部类
     */
    private static class InnerObject{
        private static Singleton5 single = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return InnerObject.single;
    }
}
(6) 内部枚举类实现 (防止反射攻击):

事实上,通过 Java 反射机制是能够实例

化构造方法为 private 的类的,这也就是

我们现在需要引入的枚举单例模式

java 复制代码
public class SingletonFactory {
    /**
     * 内部枚举类
     */
    private enum EnumSingleton{
        Singleton;
        private Singleton6 singleton;

        //枚举类的构造方法在类加载是被实例化
        private EnumSingleton(){
            singleton = new Singleton6();
        }

        public Singleton6 getInstance(){
            return singleton;
        }
    }
 
    public static Singleton6 getInstance() {
        return EnumSingleton.Singleton.getInstance();
    }
}

class Singleton6 {
    public Singleton6(){}
}

三、什么是工厂设计模式?

工厂设计模式就是用来生产对象的,在

java 中,万物皆对象,这些对象都需要

创建,如果创建的时候直接 new 该对象,

就会对该对象耦合严重,假如我们要更

换对象,所有 new 对象的地方都需要修

改一遍,这显然违背了软件设计的开闭

原则,如果我们使用工厂来生产对象,

我们就只和工厂打交道就可以了,彻底

和对象解耦,如果要更换对象,直接在

工厂里更换该对象即可,达到了与对象

解耦的目的;所以说,工厂模式最大的

优点就是:解耦

1. 简单工厂 (Simple Factory)

定义:

一个工厂方法,依据传入的参数,生成对

应的产品对象;

角色:

① 抽象产品

② 具体产品

③ 具体工厂

④ 产品使用者

使用说明:

先将产品类抽象出来,比如,苹果和梨都属

于水果,抽象出来一个水果类 Fruit,苹果和

梨就是具体的产品类,然后创建一个水果工

厂,分别用来创建苹果和梨

代码如下:

java 复制代码
// 水果接口:
public interface Fruit {
    void whatIm();
}

// 苹果类:
public class Apple implements Fruit {
    @Override
    public void whatIm() {
        System.out.println("苹果");
    }
}

// 梨类:
public class Pear implements Fruit {
    @Override
    public void whatIm() {
        System.out.println("梨");
    }
}

//水果工厂:

public class FruitFactory {
    public Fruit createFruit(String type) {
        if (type.equals("apple")) {//生产苹果
            return new Apple();
        } else if (type.equals("pear")) {//生产梨
            return new Pear();
        }
        return null;
    }
}

// 使用工厂生产产品:
public class FruitApp {
    public static void main(String[] args) {
        FruitFactory mFactory = new FruitFactory();
        Apple apple = (Apple) mFactory.createFruit("apple");//获得苹果
        Pear pear = (Pear) mFactory.createFruit("pear");//获得梨
        apple.whatIm();
        pear.whatIm();
    }
}

以上的这种方式,每当添加一种水果,就必

然要修改工厂类,违反了开闭原则;

所以简单工厂只适合于产品对象较少,且产

品固定的需求,对于产品变化无常的需求来

说显然不合适

2. 工厂方法 (Factory Method)

定义:

将工厂提取成一个接口或抽象类,具体生

产什么产品由子类决定;

角色:

① 抽象产品

② 具体产品

③ 抽象工厂

④ 具体工厂

使用说明:

和上例中一样,产品类抽象出来,这次我们

把工厂类也抽象出来,生产什么样的产品由

子类来决定

代码如下:

java 复制代码
// 水果接口、苹果类和梨类:代码和上例一样

// 抽象工厂接口:
public interface FruitFactory {
    Fruit createFruit();//生产水果
}

// 苹果工厂:
public class AppleFactory implements FruitFactory {
    @Override
    public Apple createFruit() {
        return new Apple();
    }
}

// 梨工厂:
public class PearFactory implements FruitFactory {
    @Override
    public Pear createFruit() {
        return new Pear();
    }
}

// 使用工厂生产产品:
public class FruitApp {
    public static void main(String[] args){
        AppleFactory appleFactory = new AppleFactory();
        PearFactory pearFactory = new PearFactory();
        Apple apple = appleFactory.createFruit();//获得苹果
        Pear pear = pearFactory.createFruit();//获得梨
        apple.whatIm();
        pear.whatIm();
    }
}

以上这种方式,虽然解耦了,也遵循了开闭

原则,但是如果我需要的产品很多的话,需

要创建非常多的工厂,所以这种方式的缺点

也很明显

  1. 抽象工厂 (Abstract Factory)

定义:

为创建一组相关或者是相互依赖的对象提供

的一个接口,而不需要指定它们的具体类

角色:

① 抽象产品

② 具体产品

③ 抽象工厂

④ 具体工厂

使用说明:

抽象工厂和工厂方法的模式基本一样,区别

在于,工厂方法是生产一个具体的产品,而

抽象工厂可以用来生产一组相同,有相对关

系的产品

用抽象工厂来实现:

java 复制代码
// cpu接口和实现类:
public interface Cpu {
    void run();

    class Cpu650 implements Cpu {
        @Override
        public void run() {
            System.out.println("650 也厉害");
        }
    }

    class Cpu825 implements Cpu {
        @Override
        public void run() {
            System.out.println("825 更强劲");
        }
    }
}

// 屏幕接口和实现类:
public interface Screen {
    void size();
    class Screen5 implements Screen {
        @Override
        public void size() {
            System.out.println("" + "5寸");
        }
    }

    class Screen6 implements Screen {
        @Override
        public void size() {
            System.out.println("6寸");
        }
    }
}

// 抽象工厂接口:
public interface PhoneFactory {
    Cpu getCpu();//使用的cpu
    Screen getScreen();//使用的屏幕
}

// 小米手机工厂:
public class XiaoMiFactory implements PhoneFactory {
    @Override
    public Cpu.Cpu825 getCpu() {
        return new Cpu.Cpu825();//高性能处理器
    }
 
    @Override
    public Screen.Screen6 getScreen() {
        return new Screen.Screen6();//6寸大屏
    }
}

//红米手机工厂:
public class HongMiFactory implements PhoneFactory {
    @Override
    public Cpu.Cpu650 getCpu() {
        return new Cpu.Cpu650();//高效处理器
    }

    @Override
    public Screen.Screen5 getScreen() {
        return new Screen.Screen5();//小屏手机
    }
}

// 使用工厂生产产品:
public class PhoneApp {
    public static void main(String[] args){
        HongMiFactory hongMiFactory = new HongMiFactory();
        XiaoMiFactory xiaoMiFactory = new XiaoMiFactory();
        Cpu.Cpu650 cpu650 = hongMiFactory.getCpu();
        Cpu.Cpu825 cpu825 = xiaoMiFactory.getCpu();
        cpu650.run();
        cpu825.run();
        Screen.Screen5 screen5 = hongMiFactory.getScreen();
        Screen.Screen6 screen6 = xiaoMiFactory.getScreen();
        screen5.size();
        screen6.size();
    }
}

以上例子可以看出,抽象工厂可以解决一

系列的产品生产的需求,对于大批量,多

系列的产品,用抽象工厂可以更好地管理

和扩展

4. 三种工厂方式总结

① 对于简单工厂和工厂方法来说,两者的

使用方式实际上是一样的,如果对于产

品的分类和名称是确定的,数量是相对

固定的,推荐使用简单工厂模式;

② 抽象工厂用来解决相对复杂的问题,适用于

一系列、大批量的对象生产

相关推荐
小怪瘦791 分钟前
JS实现Table表格数据跑马灯效果
开发语言·javascript·信息可视化
vvw&2 分钟前
如何在 Ubuntu 22.04 上安装并开始使用 RabbitMQ
java·linux·运维·服务器·spring·ubuntu·rabbitmq
智多星0014 分钟前
c++Qt登录页面设计
开发语言·c++·qt
吾与谁归in20 分钟前
【C#学习——特性】
开发语言·c#
code monkey.21 分钟前
【探寻C++之旅】第一章:C++入门
开发语言·c++
万琛33 分钟前
【Java-tesseract】OCR图片文本识别
java·ocr
励志成为大佬的小杨36 分钟前
c语言中的枚举类型
java·c语言·前端
yava_free39 分钟前
指定Bean加载顺序的能力
java·开发语言
程序员老冯头41 分钟前
第二十三章 C++ 继承
开发语言·数据结构·c++·算法·继承
whisperrr.1 小时前
探索JDBC:Java数据库连接的艺术与魅力
java·开发语言·数据库