JAVA面试汇总第五章 分布式与JVM和算法\设计模式等

编程知识 更新时间:2023-05-03 03:14:16

Java 分布式框架面试题合集

1.什么是 ZooKeeper?

答:ZooKeeper
是一个开源的分布式应用程序协调服务,是一个典型的分布式数据一致性解决方案。设计目的是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的系统,并以一系列简单易用的原子操作提供给用户使用。

2.ZooKeeper 提供了哪些功能?

答:ZooKeeper 主要提供以下功能:

  • 分布式服务注册与订阅:在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,比较典型的服务注册与订阅,如 Dubbo。
  • 分布式配置中心:发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到 ZooKeeper 节点上,供订阅者获取数据,实现配置信息的集中式管理和动态更新。
  • 命名服务:在分布式系统中,通过命名服务客户端应用能够根据指定名字来获取资源、服务地址和提供者等信息。
  • 分布式锁:这个主要得益于 ZooKeeper 为我们保证了数据的强一致性。

3.ZooKeeper 有几种搭建模式?

答:ZooKeeper 通常有三种搭建模式:

  • 单机模式:zoo.cfg 中只配置一个 server.id 就是单机模式了,此模式一般用在测试环境,如果当前主机宕机,那么所有依赖于当前 ZooKeeper 服务工作的其他服务器都不能进行正常工作;
  • 伪分布式模式:在一台机器启动不同端口的 ZooKeeper,配置到 zoo.cfg 中,和单机模式相同,此模式一般用在测试环境;
  • 分布式模式:多台机器各自配置 zoo.cfg 文件,将各自互相加入服务器列表,上面搭建的集群就是这种完全分布式。

4.ZooKeeper 有哪些特性?

答: ZooKeeper 特性如下:

  • 顺序一致性(Sequential Consistency):来自相同客户端提交的事务,ZooKeeper 将严格按照其提交顺序依次执行;
  • 原子性(Atomicity):于 ZooKeeper 集群中提交事务,事务将“全部完成”或“全部未完成”,不存在“部分完成”;
  • 单一系统镜像(Single System Image):客户端连接到 ZooKeeper 集群的任意节点,其获得的数据视图都是相同的;
  • 可靠性(Reliability):事务一旦完成,其产生的状态变化将永久保留,直到其他事务进行覆盖;
  • 实时性(Timeliness):事务一旦完成,客户端将于限定的时间段内,获得最新的数据。

5.以下关于 ZooKeeper 描述错误的是?

A:所有的节点都具有稳定的存储能力 B:ZooKeeper 任意节点之间都能够进行通信(消息发送 & 接收) C:为了提高性能,ZooKeeper
允许同一份数据存在一部分节点写成功,另一部分节点写失败 D:ZooKeeper 集群运行期间,只要半数以上节点存活,ZooKeeper 就能正常服务 答:C
题目解析:ZooKeeper 不允许同一份数据存在一部分节点写成功,另一部分节点写失败的情况,这不符合 ZooKeeper“一致性”的原则。

6.ZooKeeper 如何实现分布式锁?

答:ZooKeeper 实现分布式锁的步骤如下:

  • 客户端连接 ZooKeeper,并在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-10000000001,第二个为 /lock/lock-10000000002,以此类推。
  • 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
  • 执行业务代码;
  • 完成业务流程后,删除对应的子节点释放锁。

整体流程如下图所示:

7.ZooKeeper 如何实现分布式事务?

答:ZooKeeper 实现分布式事务,类似于两阶段提交,总共分为以下 4 步:

  • 客户端先给 ZooKeeper 节点发送写请求;
  • ZooKeeper 节点将写请求转发给 Leader 节点,Leader 广播给集群要求投票,等待确认;
  • Leader 收到确认,统计投票,票数过半则提交事务;
  • 事务提交成功后,ZooKeeper 节点告知客户端。

8.集群中为什么要有主节点?

答:在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,这就是主节点存在的意义。

9.Dubbo 是什么?

答:Dubbo 是一款高性能、轻量级的开源 Java RPC
框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

10.Dubbo 有哪些特性?

答:Dubbo 特性如下:

  • 面向接口代理的高性能 RPC 调用:提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节;
  • 智能负载均衡:内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量;
  • 服务自动注册与发现:支持多种注册中心服务,服务实例上下线实时感知;
  • 高度可扩展能力:遵循微内核+插件的设计原则,所有核心能力如 Protocol、Transport、Serialization 被设计为扩展点,平等对待内置实现和第三方实现;
  • 运行期流量调度:内置条件、脚本等路由策略,通过配置不同的路由规则,轻松实现灰度发布,同机房优先等功能;
  • 可视化的服务治理与运维:提供丰富服务治理、运维工具:随时查询服务元数据、服务健康状态及调用统计,实时下发路由策略、调整配置参数。

11.Dubbo 有哪些核心组件?

答:Dubbo 核心组件如下:

  • Provider:服务提供方
  • Consumer:服务消费方
  • Registry:服务注册与发现的注册中心
  • Monitor:主要用来统计服务的调用次数和调用时间
  • Container:服务的运行容器

12.Dubbo 有哪些负载均衡策略?

答:Dubbo 负责均衡策略如下:

  • 随机负载均衡(Random LoadBalance):按权重设置随机概率,在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重;
  • 轮询负载均衡(RoundRobin LoadBalance):按公约后的权重设置轮询比率,存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上;
  • 最少活跃调用数负载均衡(LeastActive LoadBalance):使用最少活跃调用数,活跃数指调用前后计数差;
  • 哈希负载均衡(ConsistentHash LoadBalance):使用哈希值转发,相同参数的请求总是发到同一提供者。

负载均衡配置如下

服务端服务级别


<dubbo:service interface=“xxx” loadbalance=“roundrobin” />

客户端服务级别


<dubbo:reference interface=“xxx” loadbalance=“roundrobin” />

服务端方法级别


<dubbo:service interface=“xxx”>
<dubbo:method name=“xxx” loadbalance=“roundrobin”/>
</dubbo:service>

客户端方法级别


<dubbo:reference interface=“xxx”>
<dubbo:method name=“xxx” loadbalance=“roundrobin”/>
</dubbo:reference>

13.Dubbo 不支持以下哪种协议?

A:dubbo://
B:rmi://
C:redis://
D:restful://

答:D

题目解析:restful 一直编程规范,并不是一种传输协议,也不被 Dubbo 支持。

14.Dubbo 默认使用什么注册中心,还有别的选择吗?

答:推荐使用 ZooKeeper 作为注册中心,还有 Nacos、Redis、Simple 注册中心(普通的 Dubbo 服务)。

15.Dubbo 支持多注册中心吗?

答:Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。

多注册中心注册:


<?xml version="1.0" encoding="UTF-8"?>

<dubbo:application name=“world” />

<dubbo:registry id=“hangzhouRegistry” address=“10.20.141.150:9090” />
<dubbo:registry id=“qingdaoRegistry” address=“10.20.141.151:9010” default=“false” />

<dubbo:service interface=“com.alibaba.hello.api.HelloService” version=“1.0.0” ref=“helloService” registry=“hangzhouRegistry,qingdaoRegistry” />

16.Dubbo 支持的连接方式有哪些?

答:Dubbo 支持的主要连接方式有:组播、直连和 ZooKeeper 等注册中心。

① 组播方式 ,不需要启动任何中心节点,只要广播地址一样,就可以互相发现。

  1. 提供方启动时广播自己的地址
  2. 消费方启动时广播订阅请求
  3. 提供方收到订阅请求时,单播自己的地址给订阅者,如果设置了 unicast=false,则广播给订阅者
  4. 消费方收到提供方地址时,连接该地址进行 RPC 调用

组播受网络结构限制,只适合小规模应用或开发阶段使用。组播地址段:224.0.0.0 ~ 239.255.255.255

配置


<dubbo:registry address=“multicast://224.5.6.7:1234” />


<dubbo:registry protocol=“multicast” address=“224.5.6.7:1234” />

为了减少广播量,Dubbo 缺省使用单播发送提供者地址信息给消费者,如果一个机器上同时启了多个消费者进程,消费者需声明
unicast=false,否则只会有一个消费者能收到消息;当服务者和消费者运行在同一台机器上,消费者同样需要声明
unicast=false,否则消费者无法收到消息,导致 No provider available for the service 异常:


<dubbo:registry address=“multicast://224.5.6.7:1234?unicast=false” />


<dubbo:registry protocol=“multicast” address=“224.5.6.7:1234”>
<dubbo:parameter key=“unicast” value=“false” />
</dubbo:registry>

② 直连方式 ,注册中心本身就是一个普通的 Dubbo 服务,可以减少第三方依赖,使整体通讯方式一致。


<dubbo:registry protocol=“zookeeper” address=“N/A” file=“./.dubbo-platform”/>

将 Simple 注册中心暴露成 Dubbo 服务:


<?xml version="1.0" encoding="UTF-8"?>


<dubbo:application name=“simple-registry” />

<dubbo:protocol port=“9090” />

<dubbo:service interface=“org.apache.dubbo.registry.RegistryService” ref=“registryService” registry=“N/A” ondisconnect=“disconnect” callbacks=“1000”>
<dubbo:method name=“subscribe”><dubbo:argument index=“1” callback=“true” /></dubbo:method>
<dubbo:method name=“unsubscribe”><dubbo:argument index=“1” callback=“false” /></dubbo:method>
</dubbo:service>


引用 Simple Registry 服务:


<dubbo:registry address=“127.0.0.1:9090” />

或者:


<dubbo:service interface=“org.apache.dubbo.registry.RegistryService” group=“simple” version=“1.0.0” … >

或者:


<dubbo:registry address=“127.0.0.1:9090” group=“simple” version=“1.0.0” />

适用性说明:此 SimpleRegistryService 只是简单实现,不支持集群,可作为自定义注册中心的参考,但不适合直接用于生产环境。

③ ZooKeeper 注册中心 ,Zookeeper 是 Apacahe Hadoop 的子项目,是一个树型的目录服务,支持变更推送,适合作为
Dubbo 服务的注册中心,工业强度较高,可用于生产环境,并推荐使用。

流程说明:

  • 服务提供者启动时:向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址
  • 服务消费者启动时:订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址,并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址
  • 监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址

支持以下功能:

  • 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息
  • 当注册中心重启时,能自动恢复注册数据,以及订阅请求
  • 当会话过期时,能自动恢复注册数据,以及订阅请求
  • 当设置 <dubbo:registry check="false" /> 时,记录失败注册和订阅请求,后台定时重试
  • 可通过 <dubbo:registry username="admin" password="1234" /> 设置 zookeeper 登录信息
  • 可通过 <dubbo:registry group="dubbo" /> 设置 zookeeper 的根节点,不设置将使用无根树
  • 支持 * 号通配符 <dubbo:reference group="*" version="*" />,可订阅服务的所有分组和所有版本的提供者

Zookeeper 使用

在 provider 和 consumer 中增加 zookeeper 客户端 jar 包依赖:



org.apache.zookeeper
zookeeper
3.3.3

Dubbo 支持 zkclient 和 curator 两种 Zookeeper 客户端实现:

注意:在 2.7.x 的版本中已经移除了 zkclient 的实现,如果要使用 zkclient 客户端,需要自行拓展。

使用 zkclient 客户端

从 2.2.0 版本开始缺省为 zkclient 实现,以提升 zookeeper
客户端的健状性。zkclient 是 Datameer 开源的一个
Zookeeper 客户端实现。

缺省配置:


<dubbo:registry … client=“zkclient” />

或:


dubbo.registry.client=zkclient

或:


zookeeper://10.20.153.10:2181?client=zkclient

需依赖或直接下载:



com.github.sgroschupf
zkclient
0.1

使用 curator 客户端

从 2.3.0 版本开始支持可选 curator 实现。Curator 是 Netflix 开源的一个 Zookeeper 客户端实现。

如果需要改为 curator 实现,请配置:


<dubbo:registry … client=“curator” />

或:


dubbo.registry.client=curator

或:


zookeeper://10.20.153.10:2181?client=curator

需依赖或直接下载:



comflix.curator
curator-framework
1.1.10

Zookeeper 单机配置:


<dubbo:registry address=“zookeeper://10.20.153.10:2181” />

或:


<dubbo:registry protocol=“zookeeper” address=“10.20.153.10:2181” />

Zookeeper 集群配置:


<dubbo:registry address=“zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181,10.20.153.12:2181” />

或:


<dubbo:registry protocol=“zookeeper” address=“10.20.153.10:2181,10.20.153.11:2181,10.20.153.12:2181” />

同一 Zookeeper,分成多组注册中心:


<dubbo:registry id=“chinaRegistry” protocol=“zookeeper” address=“10.20.153.10:2181” group=“china” />
<dubbo:registry id=“intlRegistry” protocol=“zookeeper” address=“10.20.153.10:2181” group=“intl” />

17.什么是服务熔断?

答:在应用系统服务中,当依赖服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,临时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。

18.Dubbo 可以对结果进行缓存吗?支持的缓存类型都有哪些?

答:可以,Dubbo 提供了声明式缓存,用于加速热门数据的访问速度,以减少用户加缓存的工作量。

Dubbo 支持的缓存类型有:

  • lru 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存;
  • threadlocal 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问;
  • jcache 集成,可以桥接各种缓存实现。

配置如下:


<dubbo:reference interface=“com.foo.BarService” cache=“lru” />


<dubbo:reference interface=“com.foo.BarService”>
<dubbo:method name=“findBar” cache=“lru” />
</dubbo:reference>

19.Dubbo 有几种集群容错模式?

答:Dubbo 集群容错模式如下。

① Failover Cluster

失败自动切换,当出现失败,重试其他服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。

重试次数配置如下:


<dubbo:service retries=“2” />


<dubbo:reference retries=“2” />


dubbo:reference
<dubbo:method name=“findFoo” retries=“2” />
</dubbo:reference>

② Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

③ Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

④ Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

⑤ Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。

⑥ Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

设计模式常见面试题汇总

1.说一下设计模式?你都知道哪些?

答:设计模式总共有 23 种,总体来说可以分为三大类:创建型模式( Creational Patterns )、结构型模式( Structural
Patterns )和行为型模式( Behavioral Patterns )。

分类包含关注点
创建型模式工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式关注于对象的创建,同时隐藏创建逻辑
结构型模式适配器模式、过滤器模式、装饰模式、享元模式、代理模式、外观模式、组合模式、桥接模式关注类和对象之间的组合
行为型模式责任链模式、命令模式、中介者模式、观察者模式、状态模式、策略模式、模板模式、空对象模式、备忘录模式、迭代器模式、解释器模式、访问者模式
关注对象之间的通信

下面会对常用的设计模式分别做详细的说明。

2.什么是单例模式?

答:单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例。

优点:不会频繁地创建和销毁对象,浪费系统资源。

使用场景:IO 、数据库连接、Redis 连接等。

单例模式代码实现:

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

单例模式调用代码:

public class Lesson7_3 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2); 
    }
}

程序的输出结果:true

可以看出以上单例模式是在类加载的时候就创建了,这样会影响程序的启动速度,那如何实现单例模式的延迟加载?在使用时再创建?

单例延迟加载代码:

// 单例模式-延迟加载版
class SingletonLazy {
    private static SingletonLazy instance;
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

以上为非线程安全的,单例模式如何支持多线程?

使用 synchronized 来保证,单例模式的线程安全代码:

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

3.什么是简单工厂模式?

答:简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你的需求即可。

优点

  • 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象;
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量;
  • 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。

缺点

  • 不易拓展,一旦添加新的产品类型,就不得不修改工厂的创建逻辑;
  • 产品类型较多时,工厂的创建逻辑可能过于复杂,一旦出错可能造成所有产品的创建失败,不利于系统的维护。

简单工厂示意图如下:

简单工厂 代码实现

class Factory {
    public static String createProduct(String product) {
        String result = null;
        switch (product) {
            case "Mocca":
                result = "摩卡";
                break;
            case "Latte":
                result = "拿铁";
                break;
            default:
                result = "其他";
                break;
        }
        return result;
    }
}

4.什么是抽象工厂模式?

答:抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定。

比如,以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。

抽象工厂实现代码如下:

public class AbstractFactoryTest {
   public static void main(String[] args) {
       // 抽象工厂
       String result = (new CoffeeFactory()).createProduct("Latte");
       System.out.println(result); // output:拿铁
   }
}
// 抽象工厂
abstract class AbstractFactory{
   public abstract String createProduct(String product);
}
// 啤酒工厂
class BeerFactory extends AbstractFactory{
   @Override
   public String createProduct(String product) {
       String result = null;
       switch (product) {
           case "Hans":
               result = "汉斯";
               break;
           case "Yanjing":
               result = "燕京";
               break;
           default:
               result = "其他啤酒";
               break;
       }
       return result;
   }
}
/*
 * 咖啡工厂
 */
class CoffeeFactory extends AbstractFactory{
   @Override
   public String createProduct(String product) {
       String result = null;
       switch (product) {
           case "Mocca":
               result = "摩卡";
               break;
           case "Latte":
               result = "拿铁";
               break;
           default:
               result = "其他咖啡";
               break;
       }
       return result;
   }
}

5.什么是观察者模式?

观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-
订阅(Publish/Subscribe)模式、模型-
视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。 优点

  • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
  • 观察者模式在观察目标和观察者之间建立一个抽象的耦合;
  • 观察者模式支持广播通信;
  • 观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。

缺点

  • 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

在观察者模式中有如下角色:

  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象;
  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知;
  • Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己;
  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

观察者模式实现代码如下。

1)定义观察者(消息接收方)

/*
 * 观察者(消息接收方)
 */
interface Observer {
    public void update(String message);
}
/*
 * 具体的观察者(消息接收方)
 */
class ConcrereObserver implements Observer {
    private String name;

    public ConcrereObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + ":" + message);
    }
}
2)定义被观察者(消息发送方)

/*
 * 被观察者(消息发布方)
 */
interface Subject {
    // 增加订阅者
    public void attach(Observer observer);
    // 删除订阅者
    public void detach(Observer observer);
    // 通知订阅者更新消息
    public void notify(String message);
}
/*
 * 具体被观察者(消息发布方)
 */
class ConcreteSubject implements Subject {
    // 订阅者列表(存储信息)
    private List<Observer> list = new ArrayList<Observer>();
    @Override
    public void attach(Observer observer) {
        list.add(observer);
    }
    @Override
    public void detach(Observer observer) {
        list.remove(observer);
    }
    @Override
    public void notify(String message) {
        for (Observer observer : list) {
            observer.update(message);
        }
    }
}
3)代码调用

public class ObserverTest {
    public static void main(String[] args) {
        // 定义发布者
        ConcreteSubject concreteSubject = new ConcreteSubject();
        // 定义订阅者
        ConcrereObserver concrereObserver = new ConcrereObserver("老王");
        ConcrereObserver concrereObserver2 = new ConcrereObserver("Java");
        // 添加订阅
        concreteSubject.attach(concrereObserver);
        concreteSubject.attach(concrereObserver2);
        // 发布信息
        concreteSubject.notify("更新了");
    }
}

程序执行结果如下:

老王:更新了

Java:更新了

6.什么是装饰器模式?

答:装饰器模式是指动态地给一个对象增加一些额外的功能,同时又不改变其结构。

优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

装饰器模式的关键:装饰器中使用了被装饰的对象。

比如,创建一个对象“laowang”,给对象添加不同的装饰,穿上夹克、戴上帽子…,这个执行过程就是装饰者模式,实现代码如下。

1)定义顶层对象,定义行为

interface IPerson {
    void show();
}
2)定义装饰器超类

class DecoratorBase implements IPerson{
    IPerson iPerson;
    public DecoratorBase(IPerson iPerson){
        this.iPerson = iPerson;
    }
    @Override
    public void show() {
        iPerson.show();
    }
}
3)定义具体装饰器

class Jacket extends DecoratorBase {
    public Jacket(IPerson iPerson) {
        super(iPerson);
    }
    @Override
    public void show() {
        // 执行已有功能
        iPerson.show();
        // 定义新行为
        System.out.println("穿上夹克");
    }
}
class Hat extends DecoratorBase {
    public Hat(IPerson iPerson) {
        super(iPerson);
    }
    @Override
    public void show() {
        // 执行已有功能
        iPerson.show();
        // 定义新行为
        System.out.println("戴上帽子");
    }
}
4)定义具体对象

class LaoWang implements IPerson{
    @Override
    public void show() {
        System.out.println("什么都没穿");
    }
}
5)装饰器模式调用

public class DecoratorTest {
    public static void main(String[] args) {
        LaoWang laoWang = new LaoWang();
        Jacket jacket = new Jacket(laoWang);
        Hat hat = new Hat(jacket);
        hat.show();
    }
}

7.什么是模板方法模式?

答:模板方法模式是指定义一个模板结构,将具体内容延迟到子类去实现。

优点

  • 提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中;
  • 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制并且符合开闭原则。

以给冰箱中放水果为例,比如,我要放一个香蕉:开冰箱门 → 放香蕉 → 关冰箱门;如果我再要放一个苹果:开冰箱门 → 放苹果 →
关冰箱门。可以看出它们之间的行为模式都是一样的,只是存放的水果品类不同而已,这个时候就非常适用模板方法模式来解决这个问题,实现代码如下:

/*
 * 添加模板方法 
 */
abstract class Refrigerator {
    public void open() {
        System.out.println("开冰箱门");
    }
    public abstract void put();

    public void close() {
        System.out.println("关冰箱门");
    }
}
class Banana extends Refrigerator {
    @Override
    public void put() {
        System.out.println("放香蕉");
    }
}
class Apple extends Refrigerator {
    @Override
    public void put() {
        System.out.println("放苹果");
    }
}
/*
 * 调用模板方法
 */
public class TemplateTest {
    public static void main(String[] args) {
        Refrigerator refrigerator = new Banana();
        refrigerator.open();
        refrigerator.put();
        refrigerator.close();
    }
}

程序执行结果:

开冰箱门

放香蕉

关冰箱门

8.什么是代理模式?

代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

优点

  • 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;
  • 可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务。

缺点

  • 由于使用了代理模式,因此程序的性能没有直接调用性能高;
  • 使用代理模式提高了代码的复杂度。

举一个生活中的例子:比如买飞机票,由于离飞机场太远,直接去飞机场买票不太现实,这个时候我们就可以上携程 App 上购买飞机票,这个时候携程 App
就相当于是飞机票的代理商。

代理模式实现代码如下:

/*
 * 定义售票接口
 */
interface IAirTicket {
    void buy();
}
/*
 * 定义飞机场售票
 */
class AirTicket implements IAirTicket {
    @Override
    public void buy() {
        System.out.println("买票");
    }
}
/*
 * 代理售票平台
 */
class ProxyAirTicket implements IAirTicket {
    private AirTicket airTicket;
    public ProxyAirTicket() {
        airTicket = new AirTicket();
    }
    @Override
    public void buy() {
        airTicket.buy();
    }
}
/*
 * 代理模式调用
 */
public class ProxyTest {
    public static void main(String[] args) {
        IAirTicket airTicket = new ProxyAirTicket();
        airTicket.buy();
    }
}

9.什么是策略模式?

答:策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。

优点 :遵循了开闭原则,扩展性良好。

缺点 :随着策略的增加,对外暴露越来越多。

以生活中的例子来说,比如我们要出去旅游,选择性很多,可以选择骑车、开车、坐飞机、坐火车等,就可以使用策略模式,把每种出行作为一种策略封装起来,后面增加了新的交通方式了,如超级高铁、火箭等,就可以不需要改动原有的类,新增交通方式即可,这样也符合软件开发的开闭原则。
策略模式实现代码如下:

/*
 * 声明旅行
 */
interface ITrip {
    void going();
}
class Bike implements ITrip {
    @Override
    public void going() {
        System.out.println("骑自行车");
    }
}
class Drive implements ITrip {
    @Override
    public void going() {
        System.out.println("开车");
    }
}
/*
 * 定义出行类
 */
class Trip {
    private ITrip trip;

    public Trip(ITrip trip) {
        this.trip = trip;
    }

    public void doTrip() {
        this.trip.going();
    }
}
/*
 * 执行方法
 */
public class StrategyTest {
    public static void main(String[] args) {
        Trip trip = new Trip(new Bike());
        trip.doTrip();
    }
}

程序执行的结果:

骑自行车

10.什么是适配器模式?

答:适配器模式是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够在一起工作。

优点

  • 可以让两个没有关联的类一起运行,起着中间转换的作用;
  • 灵活性好,不会破坏原有的系统。

缺点 :过多地使用适配器,容易使代码结构混乱,如明明看到调用的是 A 接口,内部调用的却是 B 接口的实现。

以生活中的例子来说,比如有一个充电器是 MicroUSB 接口,而手机充电口却是 TypeC 的,这个时候就需要一个把 MicroUSB 转换成 TypeC
的适配器,如下图所示:

适配器实现代码如下:

/*
 * 传统的充电线 MicroUSB
 */
interface MicroUSB {
    void charger();
}
/*
 * TypeC 充电口
 */
interface ITypeC {
    void charger();
}
class TypeC implements ITypeC {
    @Override
    public void charger() {
        System.out.println("TypeC 充电");
    }
}
/*
 * 适配器
 */
class AdapterMicroUSB implements MicroUSB {
    private TypeC typeC;

    public AdapterMicroUSB(TypeC typeC) {
        this.typeC = typeC;
    }

    @Override
    public void charger() {
        typeC.charger();
    }
}
/*
 * 测试调用
 */
public class AdapterTest {
    public static void main(String[] args) {
        TypeC typeC = new TypeC();
        MicroUSB microUSB = new AdapterMicroUSB(typeC);
        microUSB.charger();

    }
}

程序执行结果:

TypeC 充电

11.JDK 类库常用的设计模式有哪些?

答:JDK 常用的设计模式如下:

1)工厂模式

java.text.DateFormat 工具类,它用于格式化一个本地日期或者时间。

public final static DateFormat getDateInstance();
public final static DateFormat getDateInstance(int style);
public final static DateFormat getDateInstance(int style,Locale locale);

加密类

KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");
Cipher cipher = Cipher.getInstance("DESede");
2)适配器模式

把其他类适配为集合类

List<Integer> arrayList = java.util.Arrays.asList(new Integer[]{1,2,3});
List<Integer> arrayList = java.util.Arrays.asList(1,2,3);
3)代理模式

如 JDK 本身的动态代理。

interface Animal {
    void eat();
}
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("The dog is eating");
    }
}
class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("The cat is eating");
    }
}

// JDK 代理类
class AnimalProxy implements InvocationHandler {
    private Object target; // 代理对象
    public Object getInstance(Object target) {
        this.target = target;
        // 取得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("调用前");
        Object result = method.invoke(target, args); // 方法调用
        System.out.println("调用后");
        return result;
    }
}

public static void main(String[] args) {
    // JDK 动态代理调用
    AnimalProxy proxy = new AnimalProxy();
    Animal dogProxy = (Animal) proxy.getInstance(new Dog());
    dogProxy.eat();
}
4)单例模式

全局只允许有一个实例,比如:

Runtime.getRuntime();
5)装饰器

为一个对象动态的加上一系列的动作,而不需要因为这些动作的不同而产生大量的继承类。

java.io.BufferedInputStream(InputStream);  
java.io.DataInputStream(InputStream);  
java.io.BufferedOutputStream(OutputStream);  
java.util.zip.ZipOutputStream(OutputStream);  
java.util.Collections.checkedList(List list, Class type) ;
6)模板方法模式

定义一个操作中算法的骨架,将一些步骤的执行延迟到其子类中。

比如,Arrays.sort() 方法,它要求对象实现 Comparable 接口。

class Person implements Comparable{
    private Integer age;
    public Person(Integer age){
        this.age = age;
    }
    @Override
    public int compareTo(Object o) {
        Person person = (Person)o;
        return this.agepareTo(person.age);
    }
}
public class SortTest(){
    public static void main(String[] args){
        Person p1 = new Person(10);
        Person p2 = new Person(5);
        Person p3 = new Person(15);
        Person[] persons = {p1,p2,p3};
        //排序
        Arrays.sort(persons);
    }
}

12.IO 使用了什么设计模式?

答:IO 使用了适配器模式和装饰器模式。

  • 适配器模式:由于 InputStream 是字节流不能享受到字符流读取字符那么便捷的功能,借助 InputStreamReader 将其转为 Reader 子类,因而可以拥有便捷操作文本文件方法;
  • 装饰器模式:将 InputStream 字节流包装为其他流的过程就是装饰器模式,比如,包装为 FileInputStream、ByteArrayInputStream、PipedInputStream 等。

13.Spring 中都使用了哪些设计模式?

答:Spring 框架使用的设计模式如下。

  • 代理模式:在 AOP 中有使用
  • 单例模式:bean 默认是单例模式
  • 模板方法模式:jdbcTemplate
  • 工厂模式:BeanFactory
  • 观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用,比如,ContextStartedEvent 就是 ApplicationContext 启动后触发的事件
  • 适配器模式:Spring MVC 中也是用到了适配器模式适配 Controller

算法常用面试题汇总

1.说一下什么是二分法?使用二分法时需要注意什么?如何用代码实现?

二分法查找(Binary
Search)也称折半查找,是指当每次查询时,将数据分为前后两部分,再用中值和待搜索的值进行比较,如果搜索的值大于中值,则使用同样的方式(二分法)向后搜索,反之则向前搜索,直到搜索结束为止。

二分法使用的时候需要注意:二分法只适用于有序的数据,也就是说,数据必须是从小到大,或是从大到小排序的。

public class Lesson7_4 {
    public static void main(String[] args) {
        // 二分法查找
        int[] binaryNums = {1, 6, 15, 18, 27, 50};
        int findValue = 27;
        int binaryResult = binarySearch(binaryNums, 0, binaryNums.length - 1, findValue);
        System.out.println("元素第一次出现的位置(从0开始):" + binaryResult);
    }
    /**
     * 二分查找,返回该值第一次出现的位置(下标从 0 开始)
     * @param nums      查询数组
     * @param start     开始下标
     * @param end       结束下标
     * @param findValue 要查找的值
     * @return int
     */
    private static int binarySearch(int[] nums, int start, int end, int findValue) {
        if (start <= end) {
            // 中间位置
            int middle = (start + end) / 2;
            // 中间的值
            int middleValue = nums[middle];
            if (findValue == middleValue) {
                // 等于中值直接返回
                return middle;
            } else if (findValue < middleValue) {
                // 小于中值,在中值之前的数据中查找
                return binarySearch(nums, start, middle - 1, findValue);
            } else {
                // 大于中值,在中值之后的数据中查找
                return binarySearch(nums, middle + 1, end, findValue);
            }
        }
        return -1;
    }
}

执行结果如下:

元素第一次出现的位置(从0开始):4

2.什么是斐波那契数列?用代码如何实现?

斐波那契数列(Fibonacci Sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda
Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711…
在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1,F(2)=1,
F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用。

斐波那契数列之所以又称黄金分割数列,是因为随着数列项数的增加,前一项与后一项之比越来越逼近黄金分割的数值 0.6180339887…

斐波那契数列指的是这样一个数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
233,377,610,987,1597,2584,4181,6765,10946,17711…

斐波那契数列的特征 :第三项开始(含第三项)它的值等于前两项之和。

斐波那契数列代码实现示例,如下所示:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 斐波那契数列
        int fibonacciIndex = 7;
        int fibonacciResult = fibonacci(fibonacciIndex);
        System.out.println("下标(从0开始)" + fibonacciIndex + "的值为:" + fibonacciResult);
    }
    /**
     * 斐波那契数列
     * @param index 斐波那契数列的下标(从0开始)
     * @return int
     */
    private static int fibonacci(int index) {
        if (index == 0 || index == 1) {
            return index;
        } else {
            return fibonacci(index - 1) + fibonacci(index - 2);
        }
    }
}

执行结果如下:

下标(从0开始)7的值为:13

3.一般而言,兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?请使用代码实现。

先来分析一下,本题目

  • 第一个月:有 1 对小兔子;
  • 第二个月:小兔子变成大兔子;
  • 第三个月:大兔子下了一对小兔子;
  • 第四个月:大兔子又下了一对小兔子,上个月的一对小兔子变成了大兔子;

最后总结的规律如下列表所示:

月数123456789101112
幼仔对数1011235813213455
成兔对数01123581321345589
总对数1123581321345589144

可以看出,兔子每个月的总对数刚好符合斐波那契数列,第 12 个月的时候,总共有 144 对兔子。 实现代码如下:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 兔子的总对数
        int rabbitNumber = fibonacci(12);
        System.out.println("第 12 个月兔子的总对数是:" + rabbitNumber);
    }
    /**
     * 斐波那契数列
     * @param index 斐波那契数列的下标(从0开始)
     * @return int
     */
    private static int fibonacci(int index) {
        if (index == 0 || index == 1) {
            return index;
        } else {
            return fibonacci(index - 1) + fibonacci(index - 2);
        }
    }
}

执行结果如下:

第 12 个月兔子的总对数是:144

4.什么是冒泡排序?用代码如何实现?

冒泡排序(Bubble Sort)算法是所有排序算法中最简单、最基础的一个,它的实现思路是通过相邻数据的交换达到排序的目的。

冒泡排序的执行流程是:

  • 对数组中相邻的数据,依次进行比较;
  • 如果前面的数据大于后面的数据,则把前面的数据交换到后面。经过一轮比较之后,就能把数组中最大的数据排到数组的最后面了;
  • 再用同样的方法,把剩下的数据逐个进行比较排序,最后得到就是从小到大排序好的数据。

冒泡排序算法代码实现,如下所示:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 冒泡排序调用
        int[] bubbleNums = {132, 110, 122, 90, 50};
        System.out.println("排序前:" + Arrays.toString(bubbleNums));
        bubbleSort(bubbleNums);
        System.out.println("排序后:" + Arrays.toString(bubbleNums));
    }
    /**
     * 冒泡排序
     */
    private static void bubbleSort(int[] nums) {
        int temp;
        for (int i = 1; i < nums.length; i++) {
            for (int j = 0; j < nums.length - i; j++) {
                if (nums[j] > nums[j + 1]) {
                    temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                }
            }
            System.out.print("第" + i + "次排序:");
            System.out.println(Arrays.toString(nums));
        }
    }
}

执行结果如下:

排序前:[132, 110, 122, 90, 50]

第1次排序:[110, 122, 90, 50, 132]

第2次排序:[110, 90, 50, 122, 132]

第3次排序:[90, 50, 110, 122, 132]

第4次排序:[50, 90, 110, 122, 132]

排序后:[50, 90, 110, 122, 132]

5.什么是选择排序?用代码如何实现?

选择排序(Selection Sort)算法也是比较简单的排序算法,其实现思路是每一轮循环找到最小的值,依次排到数组的最前面,这样就实现了数组的有序排列。

比如,下面是一组数据使用选择排序的执行流程:

  • 初始化数据:18, 1, 6, 27, 15
  • 第一次排序:1, 18, 6, 27, 15
  • 第二次排序:1, 6, 18, 27, 15
  • 第三次排序:1, 6, 15, 27, 18
  • 第四次排序:1, 6, 15, 18, 27

选择排序算法代码实现,如下所示:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 选择排序调用
        int[] selectNums = {18, 1, 6, 27, 15};
        System.out.println("排序前:" + Arrays.toString(selectNums));
        selectSort(selectNums);
        System.out.println("排序后:" + Arrays.toString(selectNums));
    }
    /**
     * 选择排序
     */
    private static void selectSort(int[] nums) {
        int index;
        int temp;
        for (int i = 0; i < nums.length - 1; i++) {
            index = i;
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[index]) {
                    index = j;
                }
            }
            if (index != i) {
                temp = nums[i];
                nums[i] = nums[index];
                nums[index] = temp;
            }
            System.out.print("第" + i + "次排序:");
            System.out.println(Arrays.toString(nums));
        }
    }
}

执行结果如下:

排序前:[18, 1, 6, 27, 15]

第0次排序:[1, 18, 6, 27, 15]

第1次排序:[1, 6, 18, 27, 15]

第2次排序:[1, 6, 15, 27, 18]

第3次排序:[1, 6, 15, 18, 27]

排序后:[1, 6, 15, 18, 27]

6.什么是插入排序?用代码如何实现?

插入排序(Insertion Sort)算法是指依次把当前循环的元素,通过对比插入到合适位置的排序算法。 比如,下面是一组数据使用插入排序的执行流程:

  • 初始化数据:18, 1, 6, 27, 15
  • 第一次排序:1, 18, 6, 27, 15
  • 第二次排序:1, 6, 18, 27, 15
  • 第三次排序:1, 6, 18, 27, 15
  • 第四次排序:1, 6, 15, 18, 27

插入排序算法代码实现,如下所示:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 插入排序调用
        int[] insertNums = {18, 1, 6, 27, 15};
        System.out.println("排序前:" + Arrays.toString(insertNums));
        insertSort(insertNums);
        System.out.println("排序后:" + Arrays.toString(insertNums));
    }
    /**
     * 插入排序
     */
    private static void insertSort(int[] nums) {
        int i, j, k;
        for (i = 1; i < nums.length; i++) {
            k = nums[i];
            j = i - 1;
            // 对 i 之前的数据,给当前元素找到合适的位置
            while (j >= 0 && k < nums[j]) {
                nums[j + 1] = nums[j];
                // j-- 继续往前寻找
                j--;
            }
            nums[j + 1] = k;
            System.out.print("第" + i + "次排序:");
            System.out.println(Arrays.toString(nums));
        }
    }
}

执行结果如下:

排序前:[18, 1, 6, 27, 15]

第1次排序:[1, 18, 6, 27, 15]

第2次排序:[1, 6, 18, 27, 15]

第3次排序:[1, 6, 18, 27, 15]

第4次排序:[1, 6, 15, 18, 27]

排序后:[1, 6, 15, 18, 27]

7.什么是快速排序?用代码如何实现?

快速排序(Quick Sort)算法和冒泡排序算法类似,都是基于交换排序思想实现的,快速排序算法是对冒泡排序算法的改进,从而具有更高的执行效率。

快速排序是通过多次比较和交换来实现排序的执行流程如下:

  • 首先设定一个分界值,通过该分界值把数组分为左右两个部分;
  • 将大于等于分界值的元素放到分界值的右边,将小于分界值的元素放到分界值的左边;
  • 然后对左右两边的数据进行独立的排序,在左边数据中取一个分界值,把小于分界值的元素放到分界值的左边,大于等于分界值的元素,放到数组的右边;右边的数据也执行同样的操作;
  • 重复上述操作,当左右各数据排序完成后,整个数组也就完成了排序。

快速排序算法代码实现,如下所示:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 快速排序调用
        int[] quickNums = {18, 1, 6, 27, 15};
        System.out.println("排序前:" + Arrays.toString(quickNums));
        quickSort(quickNums, 0, quickNums.length - 1);
        System.out.println("排序后:" + Arrays.toString(quickNums));
    }
    /**
     * 快速排序
     */
    private static void quickSort(int[] nums, int left, int right) {
        int f, t;
        int ltemp = left;
        int rtemp = right;
        // 分界值
        f = nums[(left + right) / 2];
        while (ltemp < rtemp) {
            while (nums[ltemp] < f) {
                ++ltemp;
            }
            while (nums[rtemp] > f) {
                --rtemp;
            }
            if (ltemp <= rtemp) {
                t = nums[ltemp];
                nums[ltemp] = nums[rtemp];
                nums[rtemp] = t;
                --rtemp;
                ++ltemp;
            }
        }
        if (ltemp == rtemp) {
            ltemp++;
        }
        if (left < rtemp) {
            // 递归调用
            quickSort(nums, left, ltemp - 1);
        }
        if (right > ltemp) {
            // 递归调用
            quickSort(nums, rtemp + 1, right);
        }
    }
}

执行结果如下:

排序前:[18, 1, 6, 27, 15]

排序后:[1, 6, 15, 18, 27]

8.什么是堆排序?用代码如何实现?

堆排序(Heap Sort)算法是利用堆结构和二叉树的一些特性来完成排序的。
堆结构是一种树结构,准确来说是一个完全二叉树。完全二叉树每个节点应满足以下条件:

  • 如果按照从小到大的顺序排序,要求非叶节点的数据要大于等于,其左、右子节点的数据;
  • 如果按照从大到小的顺序排序,要求非叶节点的数据小于等于,其左、右子节点的数据。

可以看出,堆结构对左、右子节点的大小没有要求,只规定叶节点要和子节点(左、右)的数据满足大小关系。

比如,下面是一组数据使用堆排序的执行流程:

堆排序算法代码实现,如下所示:

public class Lesson7_4 {
    public static void main(String[] args) {
        // 堆排序调用
        int[] heapNums = {18, 1, 6, 27, 15};
        System.out.println("堆排序前:" + Arrays.toString(heapNums));
        heapSort(heapNums, heapNums.length);
        System.out.println("堆排序后:" + Arrays.toString(heapNums));
    }
    /**
     * 堆排序
     * @param nums 待排序数组
     * @param n    堆大小
     */
    private static void heapSort(int[] nums, int n) {
        int i, j, k, temp;
        // 将 nums[0,n-1] 建成大根堆
        for (i = n / 2 - 1; i >= 0; i--) {
            // 第 i 个节点,有右子树
            while (2 * i + 1 < n) {
                j = 2 * i + 1;
                if ((j + 1) < n) {
                    // 右左子树小于右子树,则需要比较右子树
                    if (nums[j] < nums[j + 1]) {
                        // 序号增加 1,指向右子树
                        j++;
                    }
                }
                if (nums[i] < nums[j]) {
                    // 交换数据
                    temp = nums[i];
                    nums[i] = nums[j];
                    nums[j] = temp;
                    // 堆被破坏,重新调整
                    i = j;
                } else {
                    // 左右子节点均大,则堆未被破坏,不需要调整
                    break;
                }
            }
        }
        for (i = n - 1; i > 0; i--) {
            // 与第 i 个记录交换
            temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
            k = 0;
            // 第 i 个节点有右子树
            while (2 * k + 1 < i) {
                j = 2 * k + 1;
                if ((j + 1) < i) {
                    // 右左子树小于右子树,则需要比较右子树
                    if (nums[j] < nums[j + 1]) {
                        // 序号增加 1,指向右子树
                        j++;
                    }
                }
                if (nums[k] < nums[j]) {
                    // 交换数据
                    temp = nums[k];
                    nums[k] = nums[j];
                    nums[j] = temp;
                    // 堆被破坏,重新调整
                    k = j;
                } else {
                    // 左右子节点均大,则堆未被破坏,不需要调整
                    break;
                }
            }
            // 输出每步排序结果
            System.out.print("第" + (n - i) + "次排序:");
            System.out.println(Arrays.toString(nums));
        }
    }
}

执行结果如下:

堆排序前:[18, 1, 6, 27, 15]

第1次排序:[18, 15, 6, 1, 27]

第2次排序:[15, 1, 6, 18, 27]

第3次排序:[6, 1, 15, 18, 27]

第4次排序:[1, 6, 15, 18, 27]

堆排序后:[1, 6, 15, 18, 27]

总结

对于应届毕业生来说,算法是大厂必考的一大重点科目,因为对于没有太多实际项目经验的应届生来说,考察的重点是逻辑思考能力和学习力,这两项能力的掌握情况都体现在算法上,因此除了本文的这些内容外,对于校招的同学来说还需要配合
LeeCode,来把算法这一关的能力构建起来,对于社招的同学来说,一般算法问到的可能性相对比较少,最常见的算法问题应该就是对冒泡和快排的掌握情况了,对于这两个算法来说,最好能到达手写代码的情况。

JVM 面试题汇总

1.什么是 JVM?它有什么作用?

答:JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,顾名思义它是一个虚拟计算机,也是 Java
程序能够实现跨平台的基础。它的作用是加载 Java 程序,把字节码翻译成机器码再交由 CPU 执行的一个虚拟计算器。

2.JVM 主要组成部分有哪些?

答:JVM 主要组成部分如下:

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)

3.JVM 是如何工作的?

答:首先程序在执行之前先要把 Java 代码(.java)转换成字节码(.class),JVM
通过类加载器(ClassLoader)把字节码加载到内存中,但字节码文件是 JVM
的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine) 将字节码翻译成底层机器码,再交由
CPU 去执行,CPU 执行的过程中需要调用本地库接口(Native Interface)来完成整个程序的运行。

4.JVM 内存布局是怎样的?

答:不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个区域:

  • 程序计数器(Program Counter Register)
  • Java 虚拟机栈(Java Virtual Machine Stacks)
  • 本地方法栈(Native Method Stack)
  • Java 堆(Java Heap)
  • 方法区(Methed Area)

① 程序计数器

程序计数器(Program Counter
Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 JVM
的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。

如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native
方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。

② Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java
方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack
Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

  • 如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。
  • 如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。

③ 本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用
Native 方法服务的。

在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 Sun HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了。

④ Java 堆

Java 堆(Java Heap)是 JVM 中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java
堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么绝对了。

如果在堆中没有内存完成实例分配,并且堆不可以再扩展时,将会抛出 OutOfMemoryError。 Java 虚拟机规范规定,Java
堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过
-Xmx 和 -Xms 控制。

⑤ 方法区

方法区(Methed Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

很多人把方法区称作“永久代”(Permanent Generation),本质上两者并不等价,只是 HotSpot 虚拟机垃圾回收器团队把 GC
分代收集扩展到了方法区,或者说是用来永久代来实现方法区而已,这样能省去专门为方法区编写内存管理的代码,但是在 JDK 8 也移除了“永久代”,使用
Native Memory 来实现方法区。

当方法无法满足内存分配需求时会抛出 OutOfMemoryError 异常。

5.在 Java 中负责字节码解释执行的是?

A:应用服务器
B:垃圾回收器
C:虚拟机
D:编译器

答:C

6.静态变量存储在哪个区?

A:栈区
B:堆区
C:全局区
D:常量区

答:C

题目解析:栈区存放函数的参数值,局部变量的值等;堆区存放的是程序员创建的对象;全局区存放全局变量和静态变量;常量区存放常量字符串。

7.垃圾回收算法有哪些?

答:垃圾回收算法如下。

  • 引用计数器算法:引用计算器判断对象是否存活的算法是这样的,给每一个对象设置一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加 1,与之相反,每当引用失效的时候就减 1。
  • 可达性分析算法:在主流的语言的主流实现中,比如 Java、C#,甚至是古老的 Lisp 都是使用的可达性分析算法来判断对象是否存活的。这个算法的核心思路就是通过一些列的“GC Roots”对象作为起始点,从这些对象开始往下搜索,搜索所经过的路径称之为“引用链”。当一个对象到 GC Roots 没有任何引用链相连的时候,证明此对象是可以被回收的。
  • 复制算法:复制算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。此算法的缺点是只能利用一半的内存空间。
  • 标记-清除算法:此算法执行分两阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
  • 标记-整理:此算法结合了“标记-清除”和“复制”两个算法的优点。分为两个阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

8.哪些对象可以作为引用链的 Root 对象?

答:引用链的 Root 对象可以为以下内容:

  • Java 虚拟机栈中的引用对象;
  • 本地方法栈中 JNI(既一般说的 Native 方法)引用的对象;
  • 方法区中类静态常量的引用对象;
  • 方法区中常量的引用对象。

9.对象引用关系都有哪些?

答:不管是引用计数法还是可达性分析算法都与对象的“引用”有关,这说明对象的引用决定了对象的生死,对象的引用关系如下。

  • 强引用:在代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象,JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
  • 弱引用:非必需对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
  • 虚引用:也称为幽灵引用或幻影引用,是最弱的一种引用关系,无法通过虚引用来获取一个对象实例,为对象设置虚引用的目的只有一个,就是当着个对象被收集器回收时收到一条系统通知。

10.内存溢出和内存泄漏的区别是什么?

答:内存溢出和内存泄漏的区别如下:

  • 内存溢出是指程序申请内存时,没有足够的内存,就会报错 OutOfMemory;
  • 内存泄漏是指垃圾对象无法回收,可以使用 Memory Analyzer 等工具排出内存泄漏。

11.垃圾回收的分类都有哪些?

答:垃圾回收的分类如下:

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

12.分代垃圾回收器的组成部分有哪些?

答:分代垃圾回收器是由:新生代(Young Generation)和老生代(Tenured
Generation)组成的,默认情况下新生代和老生代的内存比例是 1:2。

13.新生代的组成部分有哪些?

答:新生代是由:Eden、Form Survivor、To Survivor 三个区域组成的,它们内存默认占比是 8:1:1。

14.新生代垃圾回收是怎么执行的?

答:新生代垃圾回收的执行过程如下:

① Eden 区 + From Survivor 区存活着的对象复制到 To Survivor 区;
② 清空 Eden 和 From Survivor 分区;
③ From Survivor 和 To Survivor 分区交换(From 变 To,To 变 From)。

15.为什么新生代有两个 Survivor 分区?

答:当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的。

  • 如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,每次垃圾回收之后,存活的对象都会进入老生代,这样老生代的内存空间很快就被占满了,从而触发最耗时的 Full GC ,显然这样的收集器的效率是我们完全不能接受的。
  • 如果 Survivor 分区是 1 个的话,假设把两个区域分为 1:1,那么任何时候都有一半的内存空间是闲置的,显然空间利用率太低不是最佳的方案。但如果设置内存空间的比例是 8:2 ,只是看起来似乎“很好”,假设新生代的内存为 100 MB( Survivor 大小为 20 MB ),现在有 70 MB 对象进行垃圾回收之后,剩余活跃的对象为 15 MB 进入 Survivor 区,这个时候新生代可用的内存空间只剩了 5 MB,这样很快又要进行垃圾回收操作,显然这种垃圾回收器最大的问题就在于,需要频繁进行垃圾回收。
  • 如果 Survivor 分区有 2 个分区,我们就可以把 Eden、From Survivor、To Survivor 分区内存比例设置为 8:1:1 ,那么任何时候新生代内存的利用率都 90% ,这样空间利用率基本是符合预期的。再者就是虚拟机的大部分对象都符合“朝生夕死”的特性,因此每次新对象的产生都在空间占比比较大的 Eden 区,垃圾回收之后再把存活的对象方法存入 Survivor 区,如果是 Survivor 区存活的对象,那么“年龄”就 +1 ,当年龄增长到 15 (可通过 -XX:+MaxTenuringThreshold 设定)对象就升级到老生代。

经过以上对比,可以得出结论,当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的。

16.什么是 CMS 垃圾回收器?

答:CMS(Concurrent Mark Sweep)一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。

17.CMS 垃圾回收器有哪些优缺点?

答:CMS 垃圾回收器的优点是使用多线程,标记清除垃圾的,它缺点如下。

  • 对 CPU 资源要求敏感:CMS 回收器过分依赖于多线程环境,默认情况下,开启的线程数为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对用户本身的操作的影响将会很大,因为要分出一半的运算能力去执行回收器线程;
  • CMS 无法清除浮动垃圾:浮动垃圾指的是 CMS 清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除;
  • CMS 垃圾回收会产生大量空间碎片:CMS 使用的是标记-清除算法,所有在垃圾回收的时候回产生大量的空间碎片。

18.什么是 G1 垃圾回收器?

答:G1 垃圾回收器是一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS
CG,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。

G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 Region。Region
之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1
的优势更加明显。

19.垃圾回收的调优参数有哪些?

答:垃圾回收的常用调优如下:

  • -Xmx:512 设置最大堆内存为 512 M;
  • -Xms:215 初始堆内存为 215 M;
  • -XX:MaxNewSize 设置最大年轻区内存;
  • -XX:MaxTenuringThreshold=5 设置新生代对象经过 5 次 GC 晋升到老年代;
  • -XX:PretrnureSizeThreshold 设置大对象的值,超过这个值的大对象直接进入老生代;
  • -XX:NewRatio 设置分代垃圾回收器新生代和老生代内存占比;
  • -XX:SurvivorRatio 设置新生代 Eden、Form Survivor、To Survivor 占比。

常见面试题翻车合集

1.去掉 main 方法的 static 修饰符,程序会怎样?

A:程序无法编译

B:程序正常编译,正常运行

C:程序正常编译,正常运行一下马上退出

D:程序正常编译,运行时报错

答:D

题目解析:运行时异常如下:

错误: main 方法不是类 xxx 中的 static, 请将 main 方法定义为:

public static void main(String[] args)

2.以下程序运行的结果是?

public class TestClass {
    public static void main(String[] args) {
        System.out.println(getLength());
    }
    int getLength() {
        private String s = "xyz";
        int result = s.length();
        return result;
    }
}

A:3

B:2

C:4

D:程序无法编译

答:D

题目解析:成员变量 s 不能使用任何修饰符(private/protected/public)修饰,否则编译会报错。

3.以下程序有几处错误?

abstract class myAbstractClass
    private abstract String method(){};
}

A:1

B:2

C:3

D:4

答:C

题目解析:类少一个“{”类开始标签、抽象方法不能包含方法体、抽象方法访问修饰符不能为 private,因此总共有 3 处错误。

4.以下程序执行的结果是?

class A {
    public static int x;
    static {
        x = B.y + 1;
    }
}
public class B {
    public static int y = A.x + 1;
    public static void main(String[] args) {
        System.out.println(String.format("x=%d,y=%d", A.x, B.y));
    }
}

A:程序无法编译

B:程序正常编译,运行报错

C:x=1,y=2

D:x=0,y=1

答:C

5.switch 语法可以配合 return 一起使用吗?return 和 break 在 switch 使用上有何不同?

答:switch 可以配合 return 一起使用。return 和 break 的区别在于 switch 结束之后的代码,比如以下代码:

String getColor(String color) {
    switch (color) {
        case "red":
            return "红";
        case "blue":
            return "蓝";
    }
    return "未知";
}

String getColor(String color) {
    String result = "未知";
    switch (color) {
        case "red":
            result = "红";
            break;
        case "blue":
            result = "蓝";
    }
    return result;
}

对于以上这种 switch 之后没有特殊业务处理的程序来说,return 和 break 的效果是等效的。然而,对于以下这种代码:

String getColor(String color) {
    switch (color) {
        case "red":
            return "红";
        case "blue":
            return "蓝";
    }
    return "未知";
}

String getColor(String color) {
    String result = "未知";
    switch (color) {
        case "red":
            result = "红";
            break;
        case "blue":
            result = "蓝";
    }
    if (result.equals("未知")) {
        result = "透明";
    } else {
        result += "色";
    }
    return result;
}

如果 switch 之后还有特殊的业务处理,那么 return 和 break 就有很大的区别了。

6.一个栈的入栈顺序是 A、B、C、D、E 则出栈不可能的顺序是?

A:E D C B A

B:D E C B A

C:D C E A B

D:A B C D E

答:C

题目解析:栈是后进先出的,因此:

  • A 选项:入栈顺序 A B C D E 出栈顺序就是 E D C B A 是正确的;
  • B 选项:A B C D 先入栈,D 先出栈,这个时候 E 在入栈,E 在出栈,顺序 D E C B A 也是正确的;
  • C 选项:D 先出栈,说明 A B C 一定已入栈,因为题目说了入栈的顺序是 A B C D E,所以出栈的顺序一定是 C B A,而 D C E A B 的顺序 A 在 B 前面是永远不可能发生的,所以选择是 C;
  • D 选项 A B C D E 依次先入栈、出栈,顺序就是 A B C D E。

7.可以在 finally 块中使用 return吗?

答:不可以,finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。

8.FileInputStream 可以实现什么功能?

A:文件夹目录获取

B:文件写入

C:文件读取

D:文件夹目录写入

答:C

题目解析:FileInputStream 是文件读取,FileOutputStream 才是用来写入文件的,FileInputStream 和
FileOutputStream 很容易搞混。

9.以下程序打印的结果是什么?

Thread t1 = new Thread(){
    @Override
    public void run() {
        System.out.println("I'm T1.");
    }
};
t1.setPriority(3);
t1.start();
Thread t2 = new Thread(){
    @Override
    public void run() {
        System.out.println("I'm T2.");
    }
};
t2.setPriority(0);
t2.start();

答:程序报错 java.lang.IllegalArgumentException,setPriority(n) 方法用于设置程序的优先级,优先级的取值为
1-10,当设置为 0 时,程序会报错。

10.如何设置守护线程?

答:设置 Thead 类的 setDaemon(true) 方法设置当前的线程为守护线程。

守护线程的使用示例如下:

Thread daemonThread = new Thread(){
    @Override
    public void run() {
        super.run();
    }
};
// 设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();

11.以下说法中关于线程通信的说法错误的是?

A:可以调用 wait()、notify()、notifyAll() 三个方法实现线程通信

B:wait() 必须在 synchronized 方法或者代码块中使用

C:wait() 有多个重载的方法,可以指定等待的时间

D:wait()、notify()、notifyAll() 是 Object 类提供的方法,子类可以重写

答:D

题目解析:wait()、notify()、notifyAll() 都是被 final 修饰的方法,不能再子类中重写。选项 B,使用 wait()
方法时,必须先持有当前对象的锁,否则会抛出异常 java.lang.IllegalMonitorStateException。

12.ReentrantLock 默认创建的是公平锁还是非公平锁?

答:默认创建的是非公平锁,看以下源码可以得知:

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

Nonfair 为非公平的意思,ReentrantLock() 等同于代码 ReentrantLock(false)。

13.ReentrantLock 如何在一段时间内无阻塞尝试访问锁?

答:使用 tryLock(long timeout, TimeUnit unit) 方法,就可以在一段时间内无堵塞的访问锁。

14.枚举比较使用 equals 还是 ==?

答:枚举比较调用 equals 和 == 的结果是一样,查看 Enum 的源码可知 equals 其实是直接调用了 ==,源码如下:

public final boolean equals(Object other) {
    return this==other;
}

15.在 Spring 中使用 @Value 赋值静态变量为什么 null?怎么解决?

答:因为在 Springframework 框架中,当类加载器加载静态变量时,Spring 上下文尚未加载,因此类加载器不会在 bean
中正确注入静态类,导致了结果为 null。可使用 Setter() 方法给静态变量赋值,代码如下:

@Component
public class ConfigValue {
    private static String accessKey;
    public String getAccessKey() {
        return accessKey;
    }
    @Value("${accessKey}")
    public void setAccessKey(String accessKey) {
        ConfigValue.accessKey = accessKey;
    }
}
/*
 * 调用赋值变量
 */
@Component
public class TestClass {
    @Autowired
    private ConfigValue configValue;
    public void method() {
        // 读取配置文件
        configValue.getAccessKey();
    }
}

16.如何自己实现一个定时任务?

答:启动一个后台线程,循环执行任务。代码示例如下:

Thread getTocketThread = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            try {
                // 执行业务方法
                TimeUnit.HOURS.sleep(2); // 每两小时执行一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
});
if (!getTocketThread.isAlive()) {
    System.out.println("启动线程");
    getTocketThread.start();
}

17.如何定义一个不定长度的数组?

答:在 Java 中使用数组必须要指定长度,如果长度不固定可使用 ArrayList、LinkedList 等容器接收完数据,再使用 toArray()
方法转换成固定数组。

18.如何优雅的格式化百分比小数?

答:使用数字格式化类 DecimalFormat 来处理,具体实现代码如下:

double num = 0.37500;
DecimalFormat df = new DecimalFormat("0.0%");
System.out.println(df.format(num)); // 执行结果:37.5%

19.什么是跨域问题?为什么会产生跨域问题?

答:跨域问题指的是不同站点直接,使用 ajax
无法相互调用的问题。跨域问题是浏览器的行为,是为了保证用户信息的安全,防止恶意网站窃取数据,所做的限制,如果没有跨域限制就会导致信息被随意篡改和提交,会导致不可预估的安全问题,所以也会造成不同站点间“正常”请求的跨域问题。

20.跨域的解决方案有哪些?

答:常见跨域问题的解决方案如下:

  • jsonp(只支持 get 请求);

  • nginx 请求转发,把不同站点应用配置到同一个域名下;

  • 服务器端设置运行跨域访问,如果使用的是 Spring 框架可通过 @CrossOrigin 注解的方式声明某个类或方法运行跨域访问,或者配置全局跨域配置,请参考以下代码:

    // 单个跨域配置
    @CrossOrigin(origins = “http://xxx”, maxAge = 3600)
    @RestController
    public class testController{
    }

    // 全局配置
    @Configuration
    public class CorsConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurerAdapter() {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping(“/api/**”).allowedOrigins(“https://xxx”);
    }
    };
    }
    }

21.什么原因会导致 Nginx 转发时丢失部分 header 信息?该如何解决?

答:部分 header 信息丢失的原因是,丢失的 header 的 key 值中有下划线,因为 Nginx 转发时,默认会忽略带下划线的 header
信息。

解决方案有两个,一是去掉 key 值中的下划线,二是在 Nginx 的配置文件 http 中添加“underscores in headers on;”
不忽略有下划线的 header 信息。

22.如何设计一个高效的系统?

答:要设计一个高效的系统,通常要包含以下几个方面。

(1)优化代码

代码优化分为两种情况:

  • 代码问题导致系统资源消耗过多的问题,比如,某段代码导致内存溢出,往往是将 JVM 中的内存用完了,这个时候系统的内存资源消耗殆尽了,同时也会引发 JVM 频繁地发生垃圾回收,导致 CPU 100% 以上居高不下,这个时候又消耗了系统的 CPU 资源。这种情况下需要使用相应的排查工具 VisualVM 或 JConsole,找到对应的问题代码再进行优化;
  • 还有一种是非问题代码,这种代码不容易发现,比如,LinkedList 集合如果使用 for 循环遍历,则它的效率是很低的,因为 LinkedList 是链表实现的,如果使用 for 循环获取元素,在每次循环获取元素时,都会去遍历一次 List,这样会降低读的效率,这个时候应该改用 Iterator (迭代器)迭代循环该集合。
(2)设计优化

有很多问题可以通过我们的设计优化来提高程序的执行性能,比如,使用单例模式来减少频繁地创建和销毁对象所带来的性能消耗,从而提高了程序的执行性能。

(3)参数调优

JVM 和 Web
容器的参数调优,对系统的执行性能也是也很大帮助的。比如,我们的业务中会创建大量的大对象,我们可以通过设置,将这些大对象直接放进老年代,这样可以减少年轻代频繁发生小的垃圾回收(Minor
GC),减少 CPU 占用时间,从而提升了程序的执行性能。Web 容器的线程池设置以及 Linux
操作系统的内核参数设置,也对程序的运行性能有着很大的影响,我们根据自己的业务场景优化这两项内容。

(4)使用缓存

缓存的使用分为前端和后端:

  • 前端可使用浏览器缓存或者 CDN,CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术;
  • 后端缓存,使用第三方缓存 Redis 或 Memcache 来缓存查询结果,以提高查询的响应速度。
(5)优化数据库

数据库是最宝贵的资源,通常也是影响程序响应速度的罪魁祸首,它的优化至关重要,通常分为以下六个方面:

  • 合理使用数据库引擎
  • 合理设置事务隔离级别,合理使用事务
  • 正确使用 SQL 语句和查询索引
  • 合理分库分表
  • 使用数据库中间件实现数据库读写分离
  • 设置数据库主从读写分离
(6)屏蔽无效和恶意访问

前端禁止重复提交:用户提交之后按钮置灰,禁止重复提交;

用户限流,在某一时间段内只允许用户提交一次请求,比如,采取 IP 限流。

(7)搭建分布式环境,使用负载分发

可以把程序部署到多台服务器上,通过负载均衡工具,比如 Nginx,将并发请求分配到不同的服务器,从而提高了系统处理并发的能力。

加餐 | Java 面试通关攻略

面试分为三个重要的阶段:

  • 面试前准备
  • 面试中表现
  • 面试后复盘

做好这三个阶段的准备,相信一定会有很大的收获。下面来分别看看这三个阶段需要准备哪些内容。

一. 面试前准备

1. 研究待面试的公司

所谓知己知彼方能百战不殆,对待面试同样如此,企业希望招聘的人能够直接上手工作,因此会招聘那些和他们技术栈和业务方向相同或相似的应聘者。

了解了这个信息,会为我们的面试提高成功几率,那怎么才能获得这些信息呢?

获取企业的业务方向很简单,一般体现在招聘的岗位职责上,或者搜索一下就知道了;而获取招聘方的技术栈通常来说是比较困难的。以下是老王准备的一些经验,仅供参考:

  • 通过自己的关系资源,找到招聘方内部的技术人员直接询问 ,自己的关系资源包括直接关系和间接关系(朋友的朋友的朋友),比较常用的方式是发朋友圈求助;
  • 加技术群 ,技术群里面人员众多,可以在群里发言寻找,如果一个群没有,那就多加几个群继续问;
  • 通过脉脉直接找到该公司的技术人员 ,留言或者直接加好友询问;
  • 通过论坛的内推贴 ,一般发内推贴的除了 HR 就是部门的技术人员,通过这种方式联系到技术人员的几率还是挺大的。

以上的方式,面试者可根据情况使用一种或多种方式来获取自己想要的信息。

2. 打造完美的简历

除了研究应聘的企业以外,我们还要把研究的成果落实在简历上,这才是我们的真正目的。以下是准备简历时,需要注意的 8 个事项。

  • 简历要整洁美观、基础信息要全面 ,如联系方式、从业 / 学历 / 项目经验等。
  • 技术不要太庞杂 ,比如应聘的是 Java 岗位,没必要过多的对 Python、C++ 等非 Java 技术栈的经验做过多的描述,因为对于大多数技术岗位来说,面试时要求的是技术深度而不是技术广度,架构师或研发总监职位就另当别论了。
  • 提升应聘企业所要求使用的技术栈权重 ,比如某招聘企业非常重视 Spring Boot 技术的应用,面试者就应该把 该技术的掌握情况提升到简历的重要位置,让 HR 和面试技术官能够很容易地看到。
  • 提前准备相关知识点更深层次的技术问题 ,比如在简历中写了「熟悉多线程」,那面试官就有可能从多线程问到 synchronized,再从 synchronized 问到锁优化的原理等,因此需要提前准备简历中相关知识点更深层次的技术问题。
  • 项目经验向招聘企业靠拢 ,也就是说我们写的项目要尽量与招聘的企业业务方向相吻合,当然我们也不鼓励应聘者伪造项目经历的做法,这种做法如果被揭穿其后果是致命的,这里是建议面试者在写项目经验时尽量与招聘的企业业务方向相吻合。
  • 项目经验的描述要符合 STAR 法则 ,该法则指的是:情境(Situation)、任务(Task)、行动(Action)、结果(Result), 可以帮助面试者更精准地把项目内容描述清楚。
  • 增加更多技术加分元素 ,如个人技术博客、GitHub 主页、优质的论文等都属于此类加分元素。
  • 简历中最重要的一点就是:不要作假 ,大公司都有背景调查这一项,不要小瞧他们的调查能力,比你想象的还要细致。因此如果简历中有造假成分,则有 99% 的概率是会被发现的,这样,即使技术再好也不会被录用。以阿里举个例子,其内部有一个黑名单系统,如果进入了这个黑名单系统之后,阿里系的所有企业,这辈子恐怕是进不去了。

以上就是制作简历时需要注意的 8 个事项,希望面试者都能熟练掌握。

二. 面试中表现

1. 注意着装

人靠衣装马靠鞍,杨澜也说过“没有人有义务必须透过连你自己都毫不在意的邋遢外表去发现你优秀的内在”。因此即使你能力再好,也要尽量注意一下自己的形象,男士的话尽量着正装参见面试,一来显得你比较重视,二来是对招聘企业的一种尊重。

2. 注意礼仪

人都喜欢和优秀的、有素养的人交往,因此在面试中也要注意一些礼仪,这是除技术以外的一个会直接影响面试官决策的重要指标。

3. 准备自我介绍

在短短几分钟的自我介绍中,想要给面试官留下深刻的印象,一定要包含以下几点内容。

  • 描述你的技能优势 :把你掌握最擅长的技术点充分地展示出来。
  • 描述你的性格优势 :如抗压能力强、做事不抱怨等。
  • 描述你的擅长项 :例如,善于思考、做事喜欢刨根问底弄清事情的原理、学习新技术快、上手能力强等。
  • 介绍你的成就和贡献 ,比如给阿帕奇贡献了几行宝贵的代码,修复了某个框架的几个小 bug 等,都是可以瞬间展示能力的重要指标,当然学校的成就和贡献也是可以的。

4. 保持足够自信

自信可以让你在面试中正常发挥,也能让面试官更加信任你的能力,其实对待任何一件事情都是如此,只有你自己足够自信,才有可能说服别人相信你。

5. 保持热情和正念

有些公司的面试流程可能很长,例如,先 HR
面试,后面有好几轮的技术面试;或者在面试之前先填一大堆的登记信息,还有冗长的手写笔试题……这些想想就让人心烦。但越是这个时候,越要保持热情和正念,反正来都来了,既然付出了时间成本和交通成本,就把每一次的困难当做一次历练,正反都要付出相同的时间成本,还不如把自己可以掌控的事情做得更好一些。

6. Java 必须掌握的技术知识

掌握必须的 Java 知识是赢得面试的基础,以下是必备的 Java 技术点:

  • 集合
  • 数据结构和算法
    • 链表
    • 队列
    • 阻塞队列
    • 双端队列
    • 延迟队列
    • 优先级队列
    • 哈希
    • 优点
    • 如何解决哈希冲突
    • 树结构
  • 多线程
  • 线程安全
  • 六种线程池
  • 本地线程池
  • 各种锁
    • 死锁
    • synchronized
    • Lock
    • CAS
    • 解决 ABA
    • 乐观锁 / 悲观锁 / 自旋锁 / 独占锁 / 公平锁…
  • 反射 / 动态代理
  • JDK 动态代理
  • CGLIB
  • 框架
  • SSM
    • Spring
    • Spring MVC
    • MyBatis
  • Spring Boot
  • 数据和缓存
  • MySQL
    • 常用引擎
    • InnoDB
    • MyISAM
    • 存储结构
    • B+ 树
    • 黑红树
    • 二叉树
    • 索引
    • 事务 / 事务隔离性
    • 视图
    • 全局锁
    • 表锁
    • 行锁
    • 死锁
    • 日志
    • redo log
    • binlog
    • 优化
    • 慢查询分析方案
    • 优化原则
      • 最左匹配原则
      • 避免回表查询
      • 避免运算
      • 优化索引
    • 误删恢复
    • 高可用
    • 分片
      • 客户端
      • Sharding-JDBC
      • 阿里 TDDL
      • 中间件
      • MyCat
      • 网易 DDB
    • 主从分离
  • Redis
  • Java 虚拟机(JVM)
  • 内存结构
  • 垃圾回收算法
    • 计数器法
    • 可达性分析算法
    • 分代算法
  • 各种垃圾回收器
  • 分布式
  • 消息队列
    • RabbitMQ
    • Kafka
  • Dubbo
  • Zookeeper
  • 设计模式
  • 算法

下图是为面试点整理的一个脑图:

7. 为重要的问题准备答案

以下几个问题是面试环节必问的,我们应该提前为这些问题准备好自己的答案。重要的面试题如下。

  • 为什么要离职 ?这个时候不要抱怨上家公司的不好,因为没有人喜欢和背后议论别人的人做朋友,你今天说上家公司不好,明天就有可能会抱怨我们公司不好,企业也不关心你之前的公司是什么情况,而是要搞明白你离职的原因,如果只是抱怨,那你就“太年轻了”。你应该阐述自身问题,比如,可以从上班太远、路上交通成本太高等方面入手来回答这个问题。
  • 如何看待加班 ?这个问题需要表达两个观点:一是公司需要加班,义不容辞;二是尽量提高工作效率,避免加班。
  • 遇到最难的问题是什么?如何解决的 ?这个问题考察的是你技术的深度以及解决问题的思路和方法,应聘者根据自身的情况,提前准备即可。
  • 最近看什么书?平常的学习方式有哪些 ?这个问题考察的是你对学习的态度和兴趣,直接关系到你以后的进步速度,毕竟每个人最大的区别就是如何对待业余时间。
  • 还有什么问题要问我吗 ?这是出于礼貌性的问题,但你可以借此问题来传递你的上进心和忠诚度,比如你可以问以下几个问题:
  • 公司会不会提供培训?(体现上进心)
  • 公司的晋升制度是怎么样的?(自我发展和忠诚度的体现,表明我是打算以后一直在你们公司干的)
  • 公司计划安排我做什么工作?我可以提前准备什么工作?(体现上进心)

8. 添加联系方式

面试的最后,如果可能一定要主动添加面试官的联系方式,如微信,它的好处如下:

  • 可以第一时间获得录用动态
  • 通过你的朋友圈和动态可以让面试官更好得了解你
  • 可以和面试官建立更近的关系,为以后的发展提供更多的机会

三. 面试后复盘

面试后的复盘,对于你的成长有着至关重要的作用,也能避免你在一个坑里跌倒两次,面试后主要复盘的内容有两项:

  • 技术点查漏补缺
  • 面试过程中的表现复盘优化

四. 总结

通过以上的说明,我们知道如果要获得 Offer,就要做好三件大事: 面试前的准备、面试中的表现、面试后的复盘
。其中面试前要研究应聘公司的技术栈和业务方向;面试中要注意着装、注意礼仪、准备好自我介绍、保持足够的自信、保持热情和正念、并要掌握 Java
技术栈最重要的面试要点和为重要的问题提前准备好答案;面试的最后环节是加上面试官的微信,为以后赢得更好的机会;面试完要复盘技术点和面试过程中的表现,为下一次做好充分的准备。做好了以上这些内容后,相信你一定会收获一份想要的
Offer。

基于以上的内容,还准备了一份完整的脑图,如下图所示:

最后,预祝每一位学习本门课的朋友,都能找到一份自己理想中的工作。

面试流程可能很长,例如,先 HR
面试,后面有好几轮的技术面试;或者在面试之前先填一大堆的登记信息,还有冗长的手写笔试题……这些想想就让人心烦。但越是这个时候,越要保持热情和正念,反正来都来了,既然付出了时间成本和交通成本,就把每一次的困难当做一次历练,正反都要付出相同的时间成本,还不如把自己可以掌控的事情做得更好一些。

6. Java 必须掌握的技术知识

掌握必须的 Java 知识是赢得面试的基础,以下是必备的 Java 技术点:

  • 集合
  • 数据结构和算法
    • 链表
    • 队列
    • 阻塞队列
    • 双端队列
    • 延迟队列
    • 优先级队列
    • 哈希
    • 优点
    • 如何解决哈希冲突
    • 树结构
  • 多线程
  • 线程安全
  • 六种线程池
  • 本地线程池
  • 各种锁
    • 死锁
    • synchronized
    • Lock
    • CAS
    • 解决 ABA
    • 乐观锁 / 悲观锁 / 自旋锁 / 独占锁 / 公平锁…
  • 反射 / 动态代理
  • JDK 动态代理
  • CGLIB
  • 框架
  • SSM
    • Spring
    • Spring MVC
    • MyBatis
  • Spring Boot
  • 数据和缓存
  • MySQL
    • 常用引擎
    • InnoDB
    • MyISAM
    • 存储结构
    • B+ 树
    • 黑红树
    • 二叉树
    • 索引
    • 事务 / 事务隔离性
    • 视图
    • 全局锁
    • 表锁
    • 行锁
    • 死锁
    • 日志
    • redo log
    • binlog
    • 优化
    • 慢查询分析方案
    • 优化原则
      • 最左匹配原则
      • 避免回表查询
      • 避免运算
      • 优化索引
    • 误删恢复
    • 高可用
    • 分片
      • 客户端
      • Sharding-JDBC
      • 阿里 TDDL
      • 中间件
      • MyCat
      • 网易 DDB
    • 主从分离
  • Redis
  • Java 虚拟机(JVM)
  • 内存结构
  • 垃圾回收算法
    • 计数器法
    • 可达性分析算法
    • 分代算法
  • 各种垃圾回收器
  • 分布式
  • 消息队列
    • RabbitMQ
    • Kafka
  • Dubbo
  • Zookeeper
  • 设计模式
  • 算法

下图是为面试点整理的一个脑图:

7. 为重要的问题准备答案

以下几个问题是面试环节必问的,我们应该提前为这些问题准备好自己的答案。重要的面试题如下。

  • 为什么要离职 ?这个时候不要抱怨上家公司的不好,因为没有人喜欢和背后议论别人的人做朋友,你今天说上家公司不好,明天就有可能会抱怨我们公司不好,企业也不关心你之前的公司是什么情况,而是要搞明白你离职的原因,如果只是抱怨,那你就“太年轻了”。你应该阐述自身问题,比如,可以从上班太远、路上交通成本太高等方面入手来回答这个问题。
  • 如何看待加班 ?这个问题需要表达两个观点:一是公司需要加班,义不容辞;二是尽量提高工作效率,避免加班。
  • 遇到最难的问题是什么?如何解决的 ?这个问题考察的是你技术的深度以及解决问题的思路和方法,应聘者根据自身的情况,提前准备即可。
  • 最近看什么书?平常的学习方式有哪些 ?这个问题考察的是你对学习的态度和兴趣,直接关系到你以后的进步速度,毕竟每个人最大的区别就是如何对待业余时间。
  • 还有什么问题要问我吗 ?这是出于礼貌性的问题,但你可以借此问题来传递你的上进心和忠诚度,比如你可以问以下几个问题:
  • 公司会不会提供培训?(体现上进心)
  • 公司的晋升制度是怎么样的?(自我发展和忠诚度的体现,表明我是打算以后一直在你们公司干的)
  • 公司计划安排我做什么工作?我可以提前准备什么工作?(体现上进心)

8. 添加联系方式

面试的最后,如果可能一定要主动添加面试官的联系方式,如微信,它的好处如下:

  • 可以第一时间获得录用动态
  • 通过你的朋友圈和动态可以让面试官更好得了解你
  • 可以和面试官建立更近的关系,为以后的发展提供更多的机会

三. 面试后复盘

面试后的复盘,对于你的成长有着至关重要的作用,也能避免你在一个坑里跌倒两次,面试后主要复盘的内容有两项:

  • 技术点查漏补缺
  • 面试过程中的表现复盘优化

四. 总结

通过以上的说明,我们知道如果要获得 Offer,就要做好三件大事: 面试前的准备、面试中的表现、面试后的复盘
。其中面试前要研究应聘公司的技术栈和业务方向;面试中要注意着装、注意礼仪、准备好自我介绍、保持足够的自信、保持热情和正念、并要掌握 Java
技术栈最重要的面试要点和为重要的问题提前准备好答案;面试的最后环节是加上面试官的微信,为以后赢得更好的机会;面试完要复盘技术点和面试过程中的表现,为下一次做好充分的准备。做好了以上这些内容后,相信你一定会收获一份想要的
Offer。

基于以上的内容,还准备了一份完整的脑图,如下图所示:

最后,预祝每一位学习本门课的朋友,都能找到一份自己理想中的工作。

更多推荐

JAVA面试汇总第五章 分布式与JVM和算法\设计模式等

本文发布于:2023-04-30 08:25:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/06d544118fdf43acc0aea8235006ccc3.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:第五章   分布式   算法   模式   JAVA

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 113604文章数
  • 28829阅读数
  • 0评论数