《2021最新Java面试题全集-2021年第二版》不断更新完善!

    

第五章 设计模式

1:简述一下你了解的设计模式。

        所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。

      在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共23种设计模式,包括:

        Abstract Factory(抽象工厂模式),Builder(建造者模式),Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式);Facade(门面模式),Adapter(适配器模式),Bridge(桥梁模式),Composite(合成模式),Decorator(装饰模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解释器模式),Visitor(访问者模式),Iterator(迭代子模式),Mediator(调停者模式),Memento(备忘录模式),Observer(观察者模式),State(状态模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(责任链模式)。

面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:

1)工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。

2)代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。

3)适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。

4)模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。

 

基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。

 

2:简单工厂和抽象工厂有什么区别?

简单工厂模式

这个模式本身很简单而且使用在业务较简单的情况下。一般用于小项目或者具体产品很少扩展的情况(这样工厂类才不用经常更改)。

它由三种角色组成:

·       工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑,根据逻辑不同,产生具体的工厂产品。

·       抽象产品角色:它一般是具体产品继承的父类或者实现的接口。由接口或者抽象类来实现。

·       具体产品角色:工厂类所创建的对象就是此角色的实例。

·       来用类图来清晰的表示下的它们之间的关系:

 

抽象工厂模式:

先来认识下什么是产品族: 位于不同产品等级结构中,功能相关联的产品组成的家族。

可以这么说,它和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象。

而且使用抽象工厂模式还要满足一下条件:

1.  系统中有多个产品族,而系统一次只可能消费其中一族产品

2.  同属于同一个产品族的产品以其使用。

来看看抽象工厂模式的各个角色(和工厂方法的如出一辙):

抽象工厂角色: 这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在Java中它由抽象类或者接口来实现。

·       具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。在Java中它由具体的类来实现。

·       抽象产品角色:它是具体产品继承的父类或者是实现的接口。在Java中一般由抽象类或者接口来实现。

·       具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在Java中由具体的类来实现。

 

3:Java写一个单例类。

1)饿汉式单例

public class Singleton {

    private Singleton(){}

    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){

        return instance;

    }

}

2)懒汉式单例

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {}

    public static synchronized Singleton getInstance(){

        if (instance == null) instance new Singleton();

        return instance;

    }

}

实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。

3)静态内部类

public class Singleton { 

   private static class SingletonHolder { 

   private static final Singleton INSTANCE = new Singleton(); 

   } 

   private Singleton (){} 

   public static final Singleton getInstance() { 

   return SingletonHolder.INSTANCE; 

   } 

}

4)枚举

public enum Singleton { 

   INSTANCE; 

   public void whateverMethod() { 

   } 

}

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

5)双重校验锁

public class Singleton { 

   private volatile static Singleton singleton; 

   private Singleton (){} 

   public static Singleton getSingleton() { 

   if (singleton == null) { 

       synchronized (Singleton.class) { 

       if (singleton == null) { 

           singleton = new Singleton(); 

       } 

       } 

   } 

   return singleton; 

   } 

}

 

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

比如:

单例模式用于RuntimeCalendar和其他的一些类中;

工厂模式被用于各种不可变的类如 Boolean,像Boolean.valueOf

观察者模式(Observer)被用于 Swing 和很多的事件监听中;

装饰器设计模式(Decorator)被用于多个 Java I/O 类中。

 

5:观察者模式

对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

看不懂图的人端着小板凳到这里来,给你举个栗子:假设有三个人,小美,小王和小李。小美很漂亮,小王和小李是两个程序猿,时刻关注着小美的一举一动。有一天,小美说了一句:谁来陪我打游戏啊。这句话被小王和小李听到了,结果乐坏了,蹭蹭蹭,没一会儿,小王就冲到小美家门口了,在这里,小美是被观察者,小王和小李是观察者,被观察者发出一条信息,然后观察者们进行相应的处理,看代码:

public interface Person {

   //小王和小李通过这个接口可以接收到小美发过来的消息

   void getMessage(String s);

}

这个接口相当于小王和小李的电话号码,小美发送通知的时候就会拨打getMessage这个电话,拨打电话就是调用接口,看不懂没关系,先往下看

public class LaoWang implements Person {

   private String name = "小王";

   public LaoWang() {

   }

   public void getMessage(String s) {

       System.out.println(name + "接到了小美打过来的电话,电话内容是:" + s);

   }

}

public class LaoLi implements Person {

   private String name = "小李";

   public LaoLi() {

   }

   public void getMessage(String s) {

       System.out.println(name + "接到了小美打过来的电话,电话内容是:->" + s);

   }

}

代码很简单,我们再看看小美的代码:

public class XiaoMei {

   List<Person> list = new ArrayList<Person>();

    public XiaoMei(){

    }

    public void addPerson(Person person){

        list.add(person);

    }

    //遍历list,把自己的通知发送给所有暗恋自己的人

    public void notifyPerson() {

        for(Person person:list){

            person.getMessage("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!");

        }

    }

}

我们写一个测试类来看一下结果对不对

public class Test {

   public static void main(String[] args) {

       XiaoMei xiao_mei = new XiaoMei();

       LaoWang lao_wang = new LaoWang();

       LaoLi lao_li = new LaoLi();

       //小王和小李在小美那里都注册了一下

       xiao_mei.addPerson(lao_wang);

       xiao_mei.addPerson(lao_li);

       //小美向小王和小李发送通知

       xiao_mei.notifyPerson();

   }

}

 

6:装饰者模式

对已有的业务逻辑进一步的封装,使其增加额外的功能,如Java中的I/O流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。

举个栗子,我想吃三明治,首先我需要一根大大的香肠,我喜欢吃奶油,在香肠上面加一点奶油,再放一点蔬菜,最后再用两片面包夹一下,很丰盛的一顿午饭,营养又健康。那我们应该怎么来写代码呢? 首先,我们需要写一个Food类,让其他所有食物都来继承这个类,看代码:

public class Food {

   private String food_name;

   public Food() {

   }

   public Food(String food_name) {

       this.food_name = food_name;

   }

   public String make() {

       return food_name;

   };

}

代码很简单,我就不解释了,然后我们写几个子类继承它:

//面包类

public class Bread extends Food {

   private Food basic_food;

   public Bread(Food basic_food) {

       this.basic_food = basic_food;

   }

   public String make() {

       return basic_food.make()+"+面包";

   }

}

//奶油类

public class Cream extends Food {

   private Food basic_food;

   public Cream(Food basic_food) {

       this.basic_food = basic_food;

   }

   public String make() {

       return basic_food.make()+"+奶油";

   }

}

//蔬菜类

public class Vegetable extends Food {

   private Food basic_food;

   public Vegetable(Food basic_food) {

       this.basic_food = basic_food;

   }

   public String make() {

       return basic_food.make()+"+蔬菜";

   }

}

  这几个类都是差不多的,构造方法传入一个Food类型的参数,然后在make方法中加入一些自己的逻辑,如果你还是看不懂为什么这么写,不急,你看看我的Test类是怎么写的,一看你就明白了

public class Test {

   public static void main(String[] args) {

       Food food = new Bread(new Vegetable(new Cream(new Food("香肠"))));

       System.out.println(food.make());

   }

}

 看到没有,一层一层封装,我们从里往外看:最里面我new了一个香肠,在香肠的外面我包裹了一层奶油,在奶油的外面我又加了一层蔬菜,最外面我放的是面包,是不是很形象,哈哈~ 这个设计模式简直跟现实生活中一摸一样,看懂了吗? 我们看看运行结果吧。

7:适配器模式

 将两种完全不同的事物联系到一起,就像现实生活中的变压器。假设一个手机充电器需要的电压是20V,但是正常的电压是220V,这时候就需要一个变压器,将220V的电压转换成20V的电压,这样,变压器就将20V的电压和手机联系起来了。

public class Test {

   public static void main(String[] args) {

       Phone phone = new Phone();

       VoltageAdapter adapter = new VoltageAdapter();

       phone.setAdapter(adapter);

       phone.charge();

   }

}

// 手机类

class Phone {

   public static final int V = 220;// 正常电压220v是一个常量

   private VoltageAdapter adapter;

   // 充电

   public void charge() {

       adapter.changeVoltage();

   }

   public void setAdapter(VoltageAdapter adapter) {

       this.adapter = adapter;

   }

}

// 变压器

class VoltageAdapter {

   // 改变电压的功能

   public void changeVoltage() {

       System.out.println("正在充电...");

       System.out.println("原始电压" + Phone.V + "V");

       System.out.println("经过变压器转换之后的电压:" + (Phone.V - 200) + "V");

   }

}

8:什么是DAO模式?

DAOData Access Object):是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中。

用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。

 

9:讲讲常见面向对象的设计原则

1)单一职责原则:

一个类只做它该做的事情。单一职责原则想表达的就是"高内聚",写代码最终极的原则只有六个字"高内聚、低耦合",所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。

2)开闭原则:

软件实体应当对扩展开放,对修改关闭。在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。

要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱

3)依赖倒转原则:

面向接口编程。该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代

4)里氏替换原则:

任何时候都可以用子类型替换掉父类型。简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。

5)接口隔离原则:

接口要小而专,绝不能大而全。臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。

6)合成聚合复用原则:

优先使用聚合或合成关系复用代码。通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码。

7)迪米特法则:

迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。迪米特法则简单的说就是如何做到"低耦合",门面模式和调停者模式就是对迪米特法则的践行。

对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。

 

10:什么是UML

UML是统一建模语言(Unified Modeling Language)的缩写,它发表于1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用UML可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。

11:UML中有哪些常用的图?

UML定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:

用例图(use case diagram

类图(class diagram

时序图(sequence diagram

协作图(collaboration diagram

状态图(statechart diagram

活动图(activity diagram

构件图(component diagram

部署图(deployment diagram

用例图示例:

类图示例:

时序图示例:

 

在这些图形化符号中,有三种图最为重要,分别是:用例图(用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务)

 

12:什么是领域模型(domain model)?贫血模型(anaemic domain model)和充血模型(rich domain model)有什么区别?

领域模型是领域内的概念类或现实世界中对象的可视化表示,又称为概念模型或分析对象模型,它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。

贫血模型是指使用的领域对象中只有settergetter方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。有人将这里说的贫血模型进一步划分成失血模型(领域对象完全没有业务逻辑)和贫血模型(领域对象有少量的业务逻辑)。

充血模型将大多数业务逻辑和持久化放在领域对象中,业务逻辑(业务门面)只是完成对业务逻辑的封装、事务和权限等的处理。

贫血模型下组织领域逻辑通常使用事务脚本模式,让每个过程对应用户可能要做的一个动作,每个动作由一个过程来驱动。也就是说在设计业务逻辑接口的时候,每个方法对应着用户的一个操作,这种模式有以下几个有点:

1)它是一个大多数开发者都能够理解的简单过程模型(适合国内的绝大多数开发者)。

2)它能够与一个使用行数据入口或表数据入口的简单数据访问层很好的协作。

3)事务边界的显而易见,一个事务开始于脚本的开始,终止于脚本的结束,很容易通过代理(或切面)实现声明式事务。

然而,事务脚本模式的缺点也是很多的,随着领域逻辑复杂性的增加,系统的复杂性将迅速增加,程序结构将变得极度混乱。