moka一面(超详细,记录了每一个问题)

1.Arraylist和Linkedlist的区别

1、底层数据结构:

ArrayList:动态数组

LinkedList:双向链表

2、操作数据效率:

1、查找:

根据下标查找:ArrayList查找的时间复杂度是O(1),可以通过寻址公式(数组的首地址+下标*数组元素类型的大小)找到;LinkList不能通过下标查找

根据数据查找:ArrayList和LinkList的时间复杂度都是O(n),都需要整体遍历一遍

2、增加和删除:

ArrayList:向数组尾部插入或删除一个数时间复杂度是O(1)的,其他位置的时间复杂度都是O(n),因为会挪动数组,为其腾出位置。

LinkedList:在链表的头部或者尾部插入或删除一个数的时间复杂度是O(1)的,其他位置的时间复杂度都是O(n),因为首先回去遍历链表找到要插入或删除的位置。

3、内存空间的占用内存空间的占用

ArrayList:底层是数组,内存是连续的

LinkedList:底层是双向链表,需要存储数据,以及两个指针,更占用内存

4、线程安全

ArrayList和LinkedList都不是线程安全的。

如何才能保证他们的线程安全呢?

  • 在方法内使用,作为局部变量是线程安全的
  • 使用线程安全的ArrayList和LinkedList
    • List<Object> syncArrayList = Collections.sychronizedList(new ArrayList<>);
      
    • List<Object> syncLinkedList = Collections.sychronizedList(new LinkedList<>);
      

2.哪些集合类是线程安全的

1.常见的线程安全的集合

Vector:其线程安全的原理是为其所有需要保证线程安全的方法都添加了synchronized关键字,锁住了整个对象,确保了每个操作的原子性

HashTable:与Vector类似,都是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象

2.使用Colletions包装成的线程安全

  • Collections.synchronizedList:
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
  • Collections.synchronizedSet:
Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
  • Collections.synchronizedMap:
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

3、CopyOnWrite集合(快照语义):

  • CopyOnWriteArrayList(写时复制数组列表): ArrayList的线程安全变体,在每次写操作时,会先上锁,然后创建数组的副本。
  • CopyOnWriteArraySet(写时复制数组集合): 类似于CopyOnWriteArrayList,但用于Set。

4、并发集合(java.util.concurrent包):

  • ConcurrentHashMap(并发哈希映射): 高度并发和高效的哈希映射实现。
  • ConcurrentSkipListMap(并发跳表映射): 排序映射的并发实现。
  • ConcurrentSkipListSet(并发跳表集合): 排序集合的并发实现。

3.Hashmap的扩容机制

  • 第一次添加数据初始化数组的长度为16,达到了扩容阈值(0.75*数组的长度)就会进行扩容。
  • 扩容时会开一个新的数组,新数组的长度为原数组长度*2。
  • 然后开始遍历原数组的每个位置,看它是否有链表或者红黑树(也就是看e.next是否等于null),如果没有就直接使用e.hash&(newCap-1)计算新数组的索引位置。
  • 如果有就判断它是红黑树还是链表
    • 如果是红黑树就添加红黑树。
    • 如果是链表,就需要遍历链表,可能需要拆分链表,判断(e.hash&oldCap)是否为0 ,如果是0就让该元素放在新数组的原位置,如果不是0就让该元素移动到(原数组的位置+新数组大小)的位置上

4.红黑树的时间复杂度(Olog(n))

  • 查找:在红黑树中查找一个元素的平均时间复杂度为 O(log n),其中 n 是树中节点的数量。
  • 添加:先从根节点开始找到元素添加的位置,时间复杂度也是 O(log n),在插入操作中,可能需要进行一些旋转和颜色变换来维护红黑树的性质。
  • 删除:先从根节点开始找到元素添加的位置,时间复杂度也是 O(log n),删除完成后也会涉及到复杂度为O(1)的旋转调整操作。

5.创建线程的方式(runnable和callable的区别)

1、继承Thread类:

  • 创建一个类,继承自Thread类。
  • 重写run方法,定义线程执行的代码。
  • 创建类的实例并调用start方法来启动线程。
class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2、实现Runnable接口:

  • 创建一个类,实现Runnable接口。
  • 实现run方法,定义线程执行的代码
  • 创建Thread类的实例,将实现了Runnable接口的类的实例作为参数传递给Thread的构造函数。
  • 调用start方法来启动线程。
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

3、实现Callable接口

  • 创建一个类实现Callable接口,实现call方法,然后创建该类的实例
  • 使用FutureTask类来包装Callable对象(FutureTask对象封装了Callable对象的call()方法的返回值)
  • 使用FutureTask对象作为Thread对象的target创建并启动线程
  • 调用Future对象的get()方法来获取线程的执行结果
public class MyCallable implements Callable<String> { 
    @Override 
    public String call() throws Exception { 
        System.out.println("MyCallable...call..."); 
        return "OK";
    } 
public static void main(String[] args) throws 
ExecutionException, InterruptedException { 
    // 创建MyCallable对象 
    MyCallable mc = new MyCallable() ; 
    // 创建F 
    FutureTask<String> ft = new FutureTask<String>(mc) ; 
    // 创建Thread对象 
    Thread t1 = new Thread(ft) ; 
    Thread t2 = new Thread(ft) ; 
    // 调用start方法启动线程 
    t1.start(); 
    // 调用ft的get方法获取执行结果 
    String result = ft.get(); 
    // 输出 
    System.out.println(result); 
    } 
}

4、线程池创建线程

  • 定义一个类实现runnable接口
  • 创建线程池
  • 将任务提交给线程池
public class MyExecutors implements Runnable{
    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService threadPool =Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors()) ;
        // 关闭线程池
        threadPool.shutdown();
}

Runnable和Callable的区别

  • Runnable中的run方法没有返回值,Callable中的call方法有返回值,是个泛型,通过FutureTask.get()就可以获取到
  • Runnable中的run方法不能向上抛异常,而Callable中的call方法可以

6.JVM的内存模型

1、方法区(线程共享)

存储的是方法的信息(修饰符、方法名、参数、返回值等)、静态变量、常量池、编译后的代码

2、堆(线程共享)

存储的是实例和数组

当堆中没有内存空间可以分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

  • 新生代:被分为三部分,Eden区和两个大小严格相同的Survivor区
  • 老年代:主要保存一些生命周期长的对象,一般是一些老的对象

3、虚拟机栈(线程隔离)

存储的是8大基本类型+对象引用+实例方法

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
  • 每个栈由多个栈帧组成,对应每次方法调用时所占用的内存(栈帧过多会导致栈内存移出StackOverFlowError,比如递归调用)
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

4、本地方法栈(线程隔离)

存储的是本地接口库调用的方法,就是java里面native关键字修饰的方法(java作用范围达不到了,会去调用底层C/C++语言的库)。

5、程序计数器

存储的是正在执行的jvm指令,指向下一条将被执行指令的地址。

7.堆中常用的垃圾回收算法

1、标记清除算法

  • 根据可达性分析算法得出垃圾进行标记
  • 对这些标记为可回收的内容进行垃圾回收

缺点:

①效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序

②通过标记清除算法清理出来的内存,碎片化比较严重

2、复制算法

  • 将原有的内存空间一分为二,每次只用其中一块。
  • 当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再将该内存空间清空。

优点:

①在垃圾对象多的情况下,效率较高

②清理后,内存无碎片

缺点:

分配的2块内存空间,在同一时刻,只能使用一半,内存使用率较低

3、标记整理算法

标记整理算法是在标记清除算法的基础之上,做了优化改进的算法。

  • 标记垃圾
  • 将存活的对象向内存另一端移动
  • 清除边界以外的垃圾

优缺点对比:

  • 同标记清除算法想比:解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响

4、分代收集算法

其基本思想就是根据对象的生命周期将堆内存分为不同的代(新生代,老年代),然后针对每个代采用不同的垃圾回收算法。

8.项目中常用的垃圾回收器有哪些

  • 串行垃圾收集器
    • Serial串行垃圾收集器:作用于新生代,采用复制算法
    • Serial Old串行垃圾收集器:作用于老年代,采用标记-整理算法
    • 垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收完成。
  • 并行垃圾收集器(JDK8默认使用此垃圾回收器)
    • Parallel New:作用于新生代,采用复制算法
    • Parallel Old:作用于老年代,采用标记-整理算法

垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

  • CMS(并发)垃圾收集器( Concurrent Mark Sweep)
    • 是以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好;采用Mark Sweep算法
  • G1垃圾收集器
    • 应用于新生代和老年代,在JDK9之后默认使用G1
    • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
    • 采用复制算法
    • 分成三个阶段:新生代回收、并发标记、混合收集
    • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

9.@autowired和@resource的区别

1、来源不同

  • @Autowire是Spring定义的注解
  • @Resource是Java定义的注解

2、自动装配方式不同

  • @Autowire:是按照类型(byType)进行自动装配的,如果存在多个Bean再根据名称(byName)进行查找。
  • @Resource:先根据名称(byName)进行自动装配,如果按名称查找不到,再根据类型(byType)进行查找。

3、支持的注入类型

  • @Autowire:支持构造方法、setter方法和属性注入。
    • 属性注入

    • @RestController
      public class UserController {
          // 属性注入
          @Autowired
          private UserService userService;
      
          @RequestMapping("/add")
          public UserInfo add(String username, String password) {
              return userService.add(username, password);
          }
      }
      
    • 构造方法注入

    • @RestController
      public class UserController {
          // 构造方法注入
          private UserService userService;
      
          @Autowired
          public UserController(UserService userService) {
              this.userService = userService;
          }
      
          @RequestMapping("/add")
          public UserInfo add(String username, String password) {
              return userService.add(username, password);
          }
      }
      
    • Setter 注入

    • @RestController
      public class UserController {
          // Setter 注入
          private UserService userService;
      
          @Autowired
          public void setUserService(UserService userService) {
              this.userService = userService;
          }
      
          @RequestMapping("/add")
          public UserInfo add(String username, String password) {
              return userService.add(username, password);
          }
      }
      

@Resource:主要用于setter方法和属性注入。不支持构造方法注入。

4、可选属性

@Autowired:有一个可选的‘required’属性,默认为‘true’,表示被注入的Bean必须存在。如果设为‘false’,允许为‘null’

@Resource:没有类似的’required’,有七个参数

10.对IOC的理解

IOC就是控制反转,将手动创建对象的控制权交给spring容器进行管理(比如@component,@service,@repositoryd等等注解下的类交给spring容器管理)

11.MySQL的隔离级别

要解决这个问题,就要先了解一下并发事务带来哪些问题?

  • 脏读:一个事务读到了另一个事务未提交的数据。
  • 不可重复读:对同一条记录前后两次读取的结果数据不一致。(针对update操作的)。
  • 幻读:一个事务读取两次,得到的记录条数不同。(针对insert和delete操作的)。
隔离级别脏读不可重复读幻读
未提交读(Read uncommitted)
读已提交(Read committed)×
可重复读(Repeatable Read)(Mysql默认)××
串行化(Serializable)×××

12.事务的隔离性是怎么保证的(MVCC和排它锁)

MVCC

Mysql中的多版本并发控制,指维护一个数据的多个版本,使得读写没有冲突

MVCC底层实现的三个部分:

  • 隐藏字段

① trx_id(事务id):记录每一次操作的事务id,是自增的

② roll_pointer(回滚指针),指向上一个事务版本记录的地址

  • Undo log

① 回滚日志,存储老版本数据

② 版本链:通过roll_pointer指针形成一个链表,多个事务并行操作某一行记录,记录不同事务修改数据的版本

  • readView(解决的是一个事务查询选择版本的问题)

①根据readView的匹配原则和当前的一些事务id判断该访问哪个版本的数据

②不同的隔离级别快照读是不一样的,最终访问结果也不一样

RC:每一次执行快照读时都生成ReadView

RR:仅在事务第一次执行快照读时生成ReadView,后续复用

13.MySQL的索引类型

  • 唯一索引:确保列中的所有值都是唯一的,但允许存在一些null值。
CREATE TABLE example (
    id INT,
    email VARCHAR(50) UNIQUE,
    name VARCHAR(50)
);
  • 主键索引:当我们给一个字段设置主键时,它就会自动创建主键索引,用来确保每一个值都是唯一的。
CREATE TABLE example (
    id INT PRIMARY KEY,
    name VARCHAR(50)
);
  • 组合索引:指包含多个列的索引,用于加速多列的联合查询
CREATE TABLE example (
    id INT,
    name VARCHAR(50),
    age INT,
    INDEX idx_name_age (name, age)
);
  • 普通索引:是最基本的索引类型,用于加速数据的检索。与唯一索引不同,普通索引允许有重复的值。
CREATE TABLE example (
    id INT,
    name VARCHAR(50),
    INDEX idx_name (name)
);
  • 全文索引:用于在需要对文本内容进行关键字搜索的场景
CREATE TABLE example (
    id INT,
    content TEXT,
    FULLTEXT idx_content (content)
);

14、递归调用的实操

题目如下:

public class DepartmentTest {
        public static void main(String[] args) {
                List<Department> allDepartment = new ArrayList<>();
                Department dep1 = new Department(1, 0, "北京总部");
                Department dep3 = new Department(3, 1, "研发中心");
                Department dep4 = new Department(4, 3, "后端研发组");
                Department dep6 = new Department(6, 4, "后端实习生组");
                Department dep7 = new Department(7, 3, "前端研发组");
                Department dep8 = new Department(8, 1, "产品部");
                
                allDepartment.add(dep6);
                allDepartment.add(dep7);
                allDepartment.add(dep8);
                allDepartment.add(dep1);
                allDepartment.add(dep3);
                allDepartment.add(dep4);
                
    
            List<Department> subDepartments = DepartmentTest.getSub(3, allDepartment);
            for (Department subDepartment : subDepartments) {
                System.out.println(subDepartment);
            }
      }
      /**
       * 根据id,获取所有子部门列表(包括隔代子部门,一直到叶子节点)
       * 要求:不能新增参数,不能增加static变量
       * @param id
       * @return
       */
        public static List<Department> getSub(int id, List<Department> allDepartment) {
            return null;
        }
  }
  class Department {
      /** id */
      private int id;
      /** parent id */
      private int pid;
      /** 名称 */
      private String name;
      public Department(int id, int pid, String name) {
          this.id = id;
          this.pid = pid;
          this.name = name;
      }
      public int getId() {
          return id;
      }
      public void setId(int id) {
          this.id = id;
      }
      public int getPid() {
          return pid;
      }
      public void setPid(int pid) {
          this.pid = pid;
      }
      public String getName() {
          return name;
      }
      public void setName(String name) {
          this.name = name;
      }
      @Override
      public String toString() {
          return "Department{" +
                  "id=" + id +
                  ", pid=" + pid +
                  ", name='" + name + '\'' +
                  '}';
      }
  }

当时是这样写的

public static List<Department> getSub(int id, List<Department> allDepartment) {
    List<Department> departments=new ArrayList<Department>();
    for(Department department:allDepartment){
        if(department.getPid()==id){
            departments.add(department);
            List<Department> sub = getSub(department.getId(), allDepartment);
            departments.addAll(sub);
        }
    }
    return departments;
}
          ", pid=" + pid +
                  ", name='" + name + '\'' +
                  '}';
      }
  }

当时是这样写的

public static List<Department> getSub(int id, List<Department> allDepartment) {
    List<Department> departments=new ArrayList<Department>();
    for(Department department:allDepartment){
        if(department.getPid()==id){
            departments.add(department);
            List<Department> sub = getSub(department.getId(), allDepartment);
            departments.addAll(sub);
        }
    }
    return departments;
}
  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值