JAVA八股文

2025年 Java 面试八股文(20w字)_java面试八股文-CSDN博客

六、数据结构和算法

1.时间复杂度、空间复杂度

时间复杂度:指算法语句执行的次数。

空间复杂度:一个算法在运行过程中临时占用的存储空间大小,创建次数最多的变量,它被创建了多少次,那么这个算法的空间复杂度就是多少。

有个规律,如果算法语句中就有创建对象,那么这个算法的时间复杂度和空间复杂度一般一致,很好理解,算法语句被执行了多少次就创建了多少对象。

2.数组和链表结构简单对比

数组:相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标

数组的特性:

1.数组必须先定义固定长度,不能适应数据动态增减

2.当数据增加时,可能超出原先定义的元素个数,当数据减少时,造成内存浪费

3.数组查询比较方便,根据下标就可以直接找到元素,时间复杂度O(1);增加和删除比较复杂,需要移动操作数所在位置后的所有数据,时间复杂度为O(N)

链表:是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表的特性:

1.链表动态进行存储分配,可适应数据动态增减

2.插入、删除数据比较方便,时间复杂度O(1);查询必须从头开始找起,十分麻烦,时间复杂度O(N)

常见的链表:

1.单链表:通常链表每一个元素都要保存一个指向下一个元素的指针

2.双链表:每个元素既要保存到下一个元素的指针,还要保存一个上一个元素的指针

3.循环链表:在最后一个元素中下一个元素指针指向首元素

链表和数组都是在堆里分配内存

3.怎么遍历一个树

四种遍历概念

先序遍历:先访问根节点,再访问左子树,最后访问右子树。

后序遍历:先左子树,再右子树,最后根节点。

中序遍历:先左子树,再根节点,最后右子树。

层序遍历:每一层从左到右访问每一个节点。

每一个子树遍历时依然按照此时的遍历顺序。可以采用递归实现遍历。

4.冒泡排序
 

算法描述:

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

如果两个元素相等,不会再交换位置,所以冒泡排序是一种稳定排序算法。

5.快速排序

使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

对两个子序列重复上述过程,直到每个子序列内只有一个元素或者为空为止。

key值的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。

6.二分查找

算法描述:

二分查找也称折半查找,它是一种效率较高的查找方法,要求列表中的元素首先要进行有序排列。
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

七、设计模式

Java 中一般认为有 23 种设计模式,总体来说设计模式分为三大类:

创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

1.单例模式:

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。

特点

单例类只能有一个实例。

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

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

单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

四大原则:

构造私有

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

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

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

1.饿汉式(立即加载)

饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,会使Java单例实现失效)

/** 
 * @author atguigu
 * 
 * 饿汉式(立即加载) 
 */  
public class Singleton {  
  
    /** 
     * 私有构造 
     */  
    private Singleton() {  
        System.out.println("构造函数Singleton1");  
    }  
  
    /** 
     * 初始值为实例对象 
     */  
    private static Singleton single = new Singleton();  
  
    /** 
     * 静态工厂方法 
     * @return 单例对象 
     */  
    public static Singleton getInstance() {  
        System.out.println("getInstance");  
        return single;  
    }  
  
    public static void main(String[] args){  
        System.out.println("初始化");  
        Singleton instance = Singleton.getInstance();  
    }  
}  

2.懒汉式(延迟加载)

该示例虽然用延迟加载方式实现了懒汉式单例,但在多线程环境下会产生多个Singleton对象

/** 
 * @author atguigu
 * 
 * 懒汉式(延迟加载) 
 */  
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同步锁或是用同步代码块对类加同步锁,此种方式虽然解决了多个实例对象问题,但是该方式运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行。

/** 
 * @author atguigu
 * 
 * 同步锁(解决线程安全问题) 
 */  
public class Singleton3 {  
  
    /** 
     * 私有构造 
     */  
    private Singleton3() {}  
  
    /** 
     * 初始值为null 
     */  
    Private static Singleton3 single = null;  
  
    Public synchronized  static Singleton3 getInstance() {  
        
        if(single == null){  
             single = new Singleton3();  
        }  
       
        return single;  
    }  
}  

4.双重检查锁(提高同步锁的效率)

使用双重检查锁进一步做了优化,可以避免整个方法被锁,只对需要锁的代码部分加锁,可以提高执行效率。

/** 
 * @author atguigu
 * 双重检查锁(提高同步锁的效率) 
 */  
public class Singleton4 {  
  
    /** 
     * 私有构造 
     */  
    private Singleton4() {}  
  
    /** 
     * 初始值为null 
     * 加volatile关键字是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题。
     */  
    Private volatile static Singleton4 single = null;  
  
    /** 
     * 双重检查锁 
     * @return 单例对象 
     */  
    public static Singleton4 getInstance() {  
        if (single == null) {   // 解决高并发问题
            synchronized (Singleton4.class) {  
                if (single == null) {   // 判断是否为null
                    single = new Singleton4();  // 不是原子操作 分配空间 初始化赋值 引用地址
                }  
            }  
        }  
        return single;  
    }  
}  

5.静态内部类

这种方式引入了一个内部静态类(static class),静态内部类只有在调用时才会加载,它保证了Singleton 实例的延迟初始化,又保证了实例的唯一

去加载InnerObject类,同时初始化singleton 实例,所以能让getInstance() 方法线程安全。

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

静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

/** 
 * @author atguigu
 * 
 * 静态内部类(延迟加载,线程安全) 
 */  
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的类的。这也就是我们现在需要引入的枚举单例模式。

/** 
 * @author atguigu
 */  
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(){}  
}  

2.工厂设计模式:

简单工厂:

定义:

一个工厂方法,依据传入的参数,生成对应的产品对象;
角色:
1、抽象产品
2、具体产品
3、具体工厂
4、产品使用者
使用说明:

先将产品类抽象出来,比如,苹果和梨都属于水果,抽象出来一个水果类Fruit,苹果和梨就是具体的产品类,然后创建一个水果工厂,分别用来创建苹果和梨。代码如下:

水果接口:

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();  
    }  
}  

以上的这种方式,每当添加一种水果,就必然要修改工厂类,违反了开闭原则;

所以简单工厂只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。

工厂方法:

定义:

将工厂提取成一个接口或抽象类,具体生产什么产品由子类决定;
角色:
1、抽象产品
2、具体产品
3、抽象工厂
4、具体工厂
使用说明:

和上例中一样,产品类抽象出来,这次我们把工厂类也抽象出来,生产什么样的产品由子类来决定。代码如下:
水果接口、苹果类和梨类:

代码和上例一样

抽象工厂接口:

public interface FruitFactory {  
    Fruit createFruit();//生产水果  
}

苹果工厂:

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();  
    }  
}  

以上这种方式,虽然解耦了,也遵循了开闭原则,但是如果我需要的产品很多的话,需要创建非常多的工厂,所以这种方式的缺点也很明显。

抽象工厂:

定义:

为创建一组相关或者是相互依赖的对象提供的一个接口,而不需要指定它们的具体类。
角色:

抽象产品
2、具体产品
3、抽象工厂
4、具体工厂
使用说明:

抽象工厂和工厂方法的模式基本一样,区别在于,工厂方法是生产一个具体的产品,而抽象工厂可以用来生产一组相同,有相对关系的产品;重点在于一组,一批,一系列;举个例子,假如生产小米手机,小米手机有很多系列,小米note、红米note等;假如小米note生产需要的配件有825的处理器,6英寸屏幕,而红米只需要650的处理器和5寸的屏幕就可以了。用抽象工厂来实现:

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();  
    }  
}  

以上例子可以看出,抽象工厂可以解决一系列的产品生产的需求,对于大批量,多系列的产品,用抽象工厂可以更好的管理和扩展

种工厂方式总结

1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;

2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。

3.代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

为什么要用代理模式?
中介隔离作用:

在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

开闭原则,增加功能:

代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要修改已经封装好的委托类。

有哪几种代理模式?
我们有多种不同的方式来实现代理。

如果按照代理创建的时期来进行分类的话,可以分为两种:静态代理、动态代理。

静态代理是由程序员创建或特定工具自动生成源代码,再对其编译。在程序员运行之前,代理类.class文件就已经被创建了。
动态代理是在程序运行时通过反射机制动态创建的。

 静态代理(Static Proxy)

第一步:创建服务类接口

public interface BuyHouse {  
    void buyHouse();  
}  

第二步:实现服务接口

public class BuyHouseImpl implements BuyHouse {  
  
    @Override  
    public void buyHouse() {  
        System.out.println("我要买房");  
    }  
}  

第三步:创建代理类

public class BuyHouseProxy implements BuyHouse {  
  
    private BuyHouse buyHouse;  
  
    public BuyHouseProxy(final BuyHouse buyHouse) {  
        this.buyHouse = buyHouse;  
    }  
  
    @Override  
    public void buyHouse() {  
        System.out.println("买房前准备");  
        buyHouse.buyHouse();  
        System.out.println("买房后装修");  
  
    }  
}  

第四步:编写测试类

public class HouseApp {  
  
    public static void main(String[] args) {  
        BuyHouse buyHouse = new BuyHouseImpl();  
        BuyHouseProxy buyHouseProxy = new BuyHouseProxy(buyHouse);  
        buyHouseProxy.buyHouse();  
    }  
}  

静态代理总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个服务创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。  

JDK动态代理(Dynamic Proxy)
在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK在运行时为我们动态的来创建。

第一步:创建服务类接口

代码和上例一样

第二步:实现服务接口

代码和上例一样

第三步:编写动态处理器

public class DynamicProxyHandler implements InvocationHandler {  
  
    private Object object;  
  
    public DynamicProxyHandler(final Object object) {  
        this.object = object;  
    }  
  
    @Override  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
        System.out.println("买房前准备");  
        Object result = method.invoke(object, args);  
        System.out.println("买房后装修");  
        return result;  
    }  
}  

第四步:编写测试类

public class HouseApp {  
  
    public static void main(String[] args) {  
        BuyHouse buyHouse = new BuyHouseImpl();  
        BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(  
                BuyHouse.class.getClassLoader(),  
                new Class[]{BuyHouse.class},  
                new DynamicProxyHandler(buyHouse));  
        proxyBuyHouse.buyHouse();  
    }  
} 

Proxy是所有动态生成的代理的共同的父类,这个类有一个静态方法Proxy.newProxyInstance(),接收三个参数:

ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
Class[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
JDK动态代理总结:

优点:相对于静态代理,动态代理大大减少了开发任务,同时减少了对业务接口的依赖,降低了耦合度。

缺点:Proxy是所有动态生成的代理的共同的父类,因此服务类必须是接口的形式,不能是普通类的形式,因为Java无法实现多继承。

简述动态代理的原理, 常用的动态代理的实现方式:
动态代理的原理: 使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。

代理对象决定是否以及何时将方法调用转到原始对象上

动态代理的方式

基于接口实现动态代理: JDK动态代理

基于继承实现动态代理: Cglib、Javassist动态代理

 

你可能感兴趣的:(java八股文,数据结构)