服务端开发Java面试复盘篇1

Posted nuist__NJUPT

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了服务端开发Java面试复盘篇1相关的知识,希望对你有一定的参考价值。

上周投了一些简历,约了8-9家面试,其中完成了3家的第一轮面试,由于面试的是Java 的实习生,感觉问的题目都比较基础,不过有些问题回答的不是很好,在这里对回答的不太好的题目做一下总结和复盘。

目录

一、后端开发Java面试复盘

1.1、23种设计模式

1.2、数据库(MySQL)的索引失效情况

1.3、线程池的使用

1.4、ConcurrentHashMap的使用及底层原理

1.5、Spring的传递依赖问题

1.6、MySQL事务的四种隔离级别


一、后端开发Java面试复盘

1.1、23种设计模式

设计模式:就是代码设计经验的总结。使用设计模式可以提高代码的可靠性和可重用性。

设计模式共有23中,整体分为3大类,如下:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

设计模型有6大原则,具体如下

1)开放封闭原则:尽量通过扩展软件实体应对需求的改变,而不是修改原有代码

2)里氏代换原则:使用的基类可以在任何地方使用继承的字类,完美的替换基类

3)依赖倒转原则:即面向接口编程,依赖抽象而不依赖具体

4)接口隔离原则:使用多个隔离的接口替代单个接口,降低耦合

5)迪米特法则:一个类尽量减少对其他类的依赖

6)单一职责:一个方法尽量只负责一件事

单例模式,分为饿汉模式和懒汉模式,都是每个类中只创建一个实例,并提供全局访问点来访问这个实例。饿汉模型是线程安全的,懒汉模式线程不安全,需要使用双重检测锁保证线程安全,具体的Java代码实现如下:

/**
 * 单例模式:确保每个类只有一个实例,并提供全局访问点来访问这个实例
 */
//饿汉模式:线程安全
class Singleton1 
    private static Singleton1 instance = new Singleton1() ;
    public Singleton1 getInstance()
        return instance ;
    


//懒汉模式:线程不安全,需要通过双重检查锁定机制控制
class Singleton2
    private static Singleton2 instance2 = null ;
    public Singleton2 getInstance2()
        if(instance2 == null)
            instance2 = new Singleton2() ;
        
        return instance2 ;
    

//使用双重检查锁的方式保重线程安全
class Singleton3
    //使用双重检查进行初始化的实例必须使volatile关键字修饰
    private volatile static Singleton3 instance3 = null ;
    public Singleton3 getInstance3()
        if(instance3 == null)
            synchronized (Singleton3.class)
                if(instance3 == null)
                    instance3 = new Singleton3();
                
            
        
        return instance3 ;
    

public class Main 
    public static void main(String[] args) 
        Singleton1 singleton1 = new Singleton1() ;
        Singleton1 singleton2 = new Singleton1() ;
        Singleton2 singleton21 = new Singleton2() ;
        Singleton2 singleton22 = new Singleton2() ;
        Singleton1 instance1 = singleton1.getInstance();
        Singleton1 instance2 = singleton2.getInstance();
        Singleton2 instance21 = singleton21.getInstance2();
        Singleton2 instance22 = singleton22.getInstance2();
        System.out.println(instance1 == instance2);
        System.out.println(instance21 == instance22);
    

下面在了解一下工厂模式和代理模式。

工厂模式是一种创建对象的最佳方式,在创建对象时不对客户端暴露创建逻辑,通过使用一个共同的接口来指向新创建的对象,实现创建者和调用者的分离,工厂模式分为简单工厂,工厂方法和抽象工厂。我们熟知的Spring的IOC容器使用工厂模式创建Bean,只需要交给Bean进行管理即可,这样我们在业务层调接口层的方法时候就不需要再new了,可以直接注入。

1)简单工厂模式:也称为静态工厂方法,可以根据参数的不同返回不同类的实例,简单工厂模式专门定义一个类来负责创建其它类的实例,被创建的实例通常都有共同的父类。

首先创建工厂:

public interface Car 
    public void run() ;

定义工厂中的两个产品:

public class Car1 implements Car 
    @Override
    public void run() 
        System.out.println("我是小汽车");
    

public class Bus implements Car 
    @Override
    public void run() 
        System.out.println("我是大卡车");
    

创建核心工厂类,决定调用工厂中的哪一种方法,具体如下:


/**
 * 创建工厂类,在工厂类中决定调用哪一种产品
 */
public class CarFactory 
    public static Car createCar(String name)

        if("小汽车".equals(name))
            return new Car1() ;
        
        if("大卡车".equals(name))
            return new Bus() ;
        
        return null ;
    


演示创建共创对象,并调用工厂中的产品,具体如下:

/**
 * 演示简单工厂
 */
public class Factory1 
    public static void main(String[] args) 
        //通过工厂类创建工厂对象
        Car car = CarFactory.createCar("小汽车");
        Car car1 = CarFactory.createCar("大卡车");
        //调用工厂方法
        car.run();
        car1.run();
    

  • 优点:简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。明确区分了各自的职责和权力,有利于整个软件体系结构的优化。
  • 缺点:很明显工厂类集中了所有实例的创建逻辑,容易违反GRASPR的高内聚的责任分配原则。

2)工厂方法模式:也称多态性工厂模式,核心的工厂类不在负责所有产品实例的创建,而是将具体的创建交给具体的子类去做,该核心类成为一个抽象工厂角色,仅仅给出具体工厂字类必须实现的接口,而不涉及具体哪一个产品被实例化。

首先是创建工厂和工厂的两个产品,具体如下:

public interface Car 
    public void run() ;



public class Car1 implements Car 
    @Override
    public void run() 
        System.out.println("我是小汽车");
    



public class Bus implements Car 
    @Override
    public void run() 
        System.out.println("我是大卡车");
    

然后创建工厂方法调用接口,并创建工厂实例。

/**
 * 创建创建工厂方法调用接口,所有工厂产品需要实现该接口,并重写接口方法
 */
public interface Factorys 
    Car createFactory() ;



public class CarF implements Factorys 
    @Override
    public Car createFactory() 
        return new Car1() ;
    


public class BusF implements Factorys 
    @Override
    public Car createFactory() 
        return new Bus() ;
    

最后演示工厂方法的实现。

public class Factory2 
    public static void main(String[] args) 
        Car car = new CarF().createFactory();
        Car car1 = new BusF().createFactory();
        car.run() ;
        car1.run() ;
    

3)抽象工厂模式:即工厂的工厂 ,抽象工厂可以创建具体工厂,由具体工厂来生产具体产品。

首先创建第一个子工厂及其实现类,如下:

/**
 * 创建第一个工厂接口及其实现类
 */
public interface A  
    void run() ;

class CarA implements A
    @Override
    public void run() 
        System.out.println("奔驰");
    

class CarB implements A
    @Override
    public void run() 
        System.out.println("宝马");
    

创建第2个子工厂及其实现类,具体如下:

public interface B 
    void run() ;

class Animal1 implements B

    @Override
    public void run() 
        System.out.println("狗");
    

class Animal implements B

    @Override
    public void run() 
        System.out.println("猫");
    

创建一个总工厂及其实现类,由总工厂的实现类决定调用哪个工厂的哪个实例。

/**
 * 总工厂,包含创建工厂工厂的接口
 */
public interface TotalFactory 
    A createA() ;
    B createB() ;


/**
 * 总工厂的实现类,决定创建哪个工厂的哪个实例
 */
class TotalFactoryImpl implements TotalFactory

    @Override
    public A createA() 
        return new CarA() ;
    

    @Override
    public B createB() 
        return new Animal1();
    

最后演示抽象工厂模式,如下:

public class Test 
    public static void main(String[] args) 
        A a = new TotalFactoryImpl().createA();
        B b = new TotalFactoryImpl().createB();
        a.run();
        b.run() ;
    

下面我们看一下代理模式,我们正常在Spring会接触到代理模式。我们先了解一下什么是代理。

  • 通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(也就是AOP的实现)
  • 代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能,这也和Spring的(面向切面编程)很相似

我们常见的代理模式分为3种,即静态代理,JDK动态代理和CGLIB动态代理。

静态代理:简单代理模式,是动态代理的理论基础。常见使用在代理模式。
JDK 代理 : 基于接口的动态代理技术·:利用拦截器(必须实现invocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,从而实现方法增强。
CGLIB代理:基于父类的动态代理技术:动态生成一个要代理的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强。

1)静态代理

我们首先看如下一个接口类,如何在不改变接口类的基础上,在接口方法中开启和关闭事务,我们可以使用静态代理的方式。


public interface UserDao 
    void save() ;


class UserDaoImpl implements UserDao

    @Override
    public void save() 
        System.out.println("添加数据");
    

public class Test2 
    public static void main(String[] args) 
        UserDaoImpl userDao = new UserDaoImpl();
        userDao.save();
    

下面是静态代理实现的代码,如下:

public class UserDaoProxy extends UserDaoImpl 
    public UserDaoImpl userDao ;
    public UserDaoProxy(UserDaoImpl userDao)
        this.userDao = userDao ;
    

    @Override
    public void save() 
        System.out.println("开启事务");
        super.save();
        System.out.println("关闭事务");
    

public class Test2 
    public static void main(String[] args) 
        UserDaoImpl userDao = new UserDaoImpl();
        UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);
        userDaoProxy.save();

    

2)下面看一下JDK动态代理,它是基于接口的动态代理技术,用拦截器(必须实现invocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,从而实现方法增强。

首先编写可以重复使用的代理类,如下,可以重复使用,不像静态代理那样每次都要重复编写带泪类,具体如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class InvocationHandlerImpl implements InvocationHandler 
    //通过构造方法传入目标对象
    public Object target ;
    InvocationHandlerImpl(Object target)
        this.target = target ;
    
    /**
     * 动态代理实际运行的代理方法,以反射的方式创建对象
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
        System.out.println("开始");
        Object invoke = method.invoke(target, args);
        System.out.println("结束");
        return invoke ;
    
public class Test2 
    public static void main(String[] args) 
        //已构造方法的方式传入被代理的对象
        UserDaoImpl userDaoImpl = new UserDaoImpl() ;
        InvocationHandlerImpl invocationHandler = new InvocationHandlerImpl(userDaoImpl);
        //类加载器
        ClassLoader classLoader = userDaoImpl.getClass().getClassLoader();
        Class<?>[] interfaces = userDaoImpl.getClass().getInterfaces();
        UserDao proxyInstance = (UserDao)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
        proxyInstance.save();

    

3)最后我们看一下CGLIB动态代理,基于父类的动态代理技术:动态生成一个要代理的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强。

简单地说就是实现MethodInterceptor接口并重写intercept()方法实现动态代理。

1.2、数据库(mysql)的索引失效情况

1)查询条件有or关键字(必须所有查询条件都有索引)

2)模糊查询like以%开头

3)索引列上的条件where部分涉及计算或者函数

4)复合索引缺少左列字段

5)数据库认为全表扫描比索引更高效

1.3、线程池的使用

创建线程常见的几种方法包括继承Thread类并重写run()方法,实现Runnable()接口或实现Collable()等。一般来说,我们使用传统的方法每次创建和销毁线程的开销都比较大,故我们可以使用线程池的方式,减小开销。

使用线程池主要有如下的优势:

1)提高效率,创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。
2)减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
3)提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。
 

Executors类(并发包)提供了4种创建线程池方法,这些方法最终都是通过配置ThreadPoolExecutor的不同参数,来达到不同的线程管理效果。

推荐通过 new ThreadPoolExecutor() 的写法创建线程池,这样写线程数量更灵活开发中多数用这个类创建线程。

该类包含如下核心参数:核心线程数,最大线程数,闲置超时时间,超时时间的单位,线程池中的任务队列,线程工厂,拒绝策略。

 

最后一个参数为拒绝策略,一半包含四种拒绝策略,如下:

1. AbortPolicy

当任务添加到线程池中被拒绝时,直接丢弃任务,并抛出RejectedExecutionException异常

2. DiscardPolicy

当任务添加到线程池中被拒绝时,丢弃被拒绝的任务,不抛异常

3. DiscardOldestPolicy

当任务添加到线程池中被拒绝时,丢弃任务队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中

 4. CallerRunsPolicy

被拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务。

总结:就是被拒绝的任务,直接在主线程中运行,不再进入线程池。

下面看一个demo,创建线程池,并设置最大线程数目为2,设置拒绝策略为CallerRunsPolicy。

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test 
    public static void main(String[] args) 
        // 创建单线程-线程池,任务依次执行
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2,
                60, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(2),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) 
            //创建任务
            Runnable runnable = new Runnable() 
                @Override
                public void run() 
                    try 
                        Thread.sleep(20);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    System.out.println(Thread.currentThread().getName());
                
            ;
            // 将任务交给线程池管理
            threadPoolExecutor.execute(runnable);
        
    

1.4、ConcurrentHashMap的使用及底层原理

Map包括HashMap和HashTable。

HashMap性能高,但不是线程安全:
HashMap的性能比较高,但是HashMap不是线程安全的,在并发环境下,可能会形成环状链表导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。

HashTable是线程安全的,但性能差:
HashTable和HashMap的实现原理几乎一样,差别:HashTable不允许key和value为null。
HashTable是线程安全的,但是HashTable线程安全的策略实现代价却比较大,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,这就导致性能比较低。

ConcurrentHashMap是线程安全的且性能高:

JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。

JDK1.8版本:做了2点修改,取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。
 

1.5、Spring的传递依赖问题

maven环境存在的依赖冲突问题,可以在pom文件中使用<exlusions>标签排除依赖,主动断开依赖的资源,被排除的资源无需指定版本。

在Spring中解决循环依赖问题,可以使用三级缓存,三级缓存如下:

 

1.6、MySQL事务的四种隔离级别

MySQL支持四种事务隔离级别,默认的事务隔离级别为repeatable read,Oracle数据库默认的隔离级别是read committed。

四种隔离级别分别为读未提交,读已提交,可重复读, 序列化/串行化。其中,读未提交是最低的隔离级别,序列化隔离级别最高。

1.读未提交(read uncommitted):没有提交就读取到了。
2.读已提交(read committed):已经提交的才能读取。
3.可重复读(repeatable read):事务结束之前。永远读取不到真实数据,提交了也读取不到,读取的永远都是最初的数据,即假象。
4.序列化(serializable):表示事务排队,不能并发,每次读取的都是最真实的数据。

同时运行多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种并发问题。
1-脏读:对于两个事务T1和T2,T1读取了已经被T2更新但还没有提交的字段,之后,若T2回滚,则T1读取的数据就是临时且无效的。
2-不可重复读:对于两个事务T1和T2,T1读取了该字段,但是T2更新了该字段,T1再次读取这个字段,值就不同了。
3-幻读:对于两个事务T1和T2,T1从表中读取了一些字段,T2在表中插入了一些新的行,T1再次读取该表发现多几行。

read uncommitted:可以出现脏读,幻读,不可重复读。
read committed:避免脏读,出现幻读和不可重复读。
repeatable read:避免脏读和不可重复读,出现幻读。
serializable:避免脏读,幻读,不可重复读。

以上是关于服务端开发Java面试复盘篇1的主要内容,如果未能解决你的问题,请参考以下文章

服务端开发之Java备战秋招面试篇2-HashMap底层原理篇

服务端开发Java之备战秋招面试篇1

服务端开发之Java备战秋招面试篇5

服务端开发之Java备战秋招面试篇6-Java各种并发锁

校招实习面试实战,顺丰科技Java工程师面试复盘总结

大数据开发工师面试复盘