扬仔的杂货铺

分享微不足道的经验,记录稀奇古怪的玩意

0%

面试试题汇总

面向对象和面向过程的区别

  • 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消 耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
  • 面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特 性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能 比面向过程低。

面向过程比较直接高效(注重步骤和顺序)

面向对象更易于复用、扩展和维护(注重有哪些参与者以及各自需要做什么)

面向对象三大特点

  • 封装

    能把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。private protected public

  • 继承

    指可以让某个类型的对象获得另一个类型的对象的属性的方法

    它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

    继承概念的实现方式有两类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;

  • 多态

    就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。

JDKJRE

  • JDK:Java开发工具,不仅拥有JRE,还拥有Java编译器和Java工具,比如说Jconsole、Javadoc
  • JRE:Java运行时环境

构造器能否重写?

不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。

重载和重写的区别

区别点 重载方法 重写方法
发生范围 同一个类 子类
参数列表 必须修改 一定不能修改
返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等
异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
访问修饰符 可修改 一定不能做更严格的限制(可以降低限制)
发生阶段 编译期 运行期

String、StringBuffer和StringBuilder的区别是什么?String为什么不可变?

源码中的定义

1
2
3
4
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];

private final char value[];

String类中使用final关键字修饰字符数组来保存字符串,private final char value[];,所以String对象是不可变的。

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder中也是使用字符数组保存字符串char[] value但是没有用 final 关键字修饰,所以这两种对象都是可变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;

/**
* The count is the number of characters used.
*/
int count;

/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}

对于三者使用的总结:

  1. 操作少量的数据: 适用String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer

List和Set的区别

简单来说:

  • List:有序的(按照对象进入的顺序保存对象),可重复的(允许多个Null元素对象),可以使用Iterator迭代器取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素。
  • Set:是无序的,不可重复的。最多允许一个Null元素的对象,取元素时只能使用Iterator迭代器取得所有元素,再逐一遍历各个元素。

hashCode与equals

  • equals是用来对比两个对象是否相等的方法。如果不进行重写的话,就是两个对象使用==来比较。(引用对象比较引用的地址,基本数据类型比较数值是否相同)

  • hashCode()的作用是获取对象的哈希码,返回的是一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。Java中的所有类都包含有hashCode()函数。

    哈希表存储的是键值对,可以通过对象的哈希码从哈希表中快速找到对象在堆内存中的存储位置。

为什么要有hashCode?

对象加入HashSet时,先计算对象的哈希码,然后查看哈希码对应的位置有没有值,如果有值,进行equals比较,相同加入失败,不相同重新哈希到其他位置。这样就可以大大减少equals的次数,加快了执行速度。

  • 如果两个对象相等,则hashcode一定也是相同的
  • 两个对象相等,对两个对象分别调用equals方法都返回true
  • 两个对象有相同的hashcode值,他们也不一定是相等的
  • 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
  • hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(及时这两个对象指向相同的数据)

ArrayList和LinkedList的区别

  • ArrayList:基于动态数组,使用连续内存存储,适合下标访问(随机访问)

    扩容机制:因为数组长度固定,超出长度存储数据时需要新建数组,每次扩容1.5倍(int newCapacity = oldCapacity + (oldCapacity >> 1);),然后将老数组的数据拷贝到新数组

    插入机制:

    1
    2
    3
    //如果不是尾部插入,会将数组的后半部分复制到靠后一位,然后将要插入的数据保存到空出来的位置
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    elementData[index] = element;

    如果使用尾插法并指定初始容量可以极大提升性能,甚至超过LinkedList(因为LinkedList需要创建大量的node对象)

  • LinkedList:基于链表,可以存储在分散的内存中。适合做数据插入及删除操作,不适合查询(需要逐一遍历)

    遍历LInkedList建议使用Iterator迭代器,使用for循环和indexOf等都要遍历整个列表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //使用get时查看索引位置在前半部分还是后半部分,决定从前向后遍历还是从后向前遍历
    public int lastIndexOf(Object o) {
    int index = size;
    if (o == null) {
    for (Node<E> x = last; x != null; x = x.prev) {
    index--;
    if (x.item == null)
    return index;
    }
    } else {
    for (Node<E> x = last; x != null; x = x.prev) {
    index--;
    if (o.equals(x.item))
    return index;
    }
    }
    return -1;
    }

HashMap和HashTable的区别?底层实现是什么?

  • HashMap是线程不安全的,允许Key和Value为null
  • HashTable是线程安全的,因为里面的方法都用synchronized关键字进行包裹,所以他的效率很低。后来都是使用ConcurrentHashMap,bu允许Key和Value为null

底层实现:数组加链表

JDK8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在

保存Key的过程

  1. 对key进行两次hashCode计算,然后除以数组长度取余数,获取在数组中保存的下标
  2. 如果该下标没有冲突,直接保存;如果冲突,进行Equals比较,相同说明Key相同,进行Value值的替换,不相同加入到链表的末尾
  3. 如果链表高度达到8,并且数组长度达到64,将数组转变为红黑树(平衡二叉树,查询效率高),如果长度低于6的时候将红黑树转回链表

如果key为null,直接存在下标为0的位置(因为不好计算hashcode)

ConcurrentHashMap

HashTable采用的是所有方法都加上Synchronized关键字的全局锁。

ConcurrentHashMap采用的是分段锁。

  • JDK7使用的是Segment分段锁,在每一个分段加入锁,这样的话锁的力度就小一些,相当于每一个分段下有一个小数组加链表结构

    • 写入的时候有锁
    • get方法无需加锁,使用volatile修饰保证线程共享
  • JDK8的时候使用的Synchronized(数组扩容,hash冲突的时候使用) + CAS + Node + 红黑树,Node的val和next都用volatile修饰,保证数据的可见性。在查找、替换、复制操作时都使用CAS(乐观锁,操作数据时校验)。

    锁:锁链表的head节点,不影响其他元素的读写,锁的粒度更细,效率更高。扩容时,阻塞所有的读写操作、并发扩容

    读操作无锁:

    • Node的val和next使用volatile修饰,读写线程对该变量互相可见
    • 数组用volatile修饰,保证扩容时被读线程感知

创建线程的4种方式

  1. 继承Thread

    Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己创建的类直接extends Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法

    • 优点:代码简单
    • 缺点:该类无法继承别的类
  2. 实现Runnable接口

    Java中的类属于单继承,如果自己的类已经extends另一个类,就无法直接extends Thread,但是一个类继承一个类的同时,是可以实现多个接口的。

    • 优点:继承其他类。统一实现该接口的实例可以共享资源
    • 缺点:代码复杂
  3. 实现Callable接口

    实现Runnable和实现Callable接口的方式基本相同,不过Callable接口中的call()方法有返回值,Runnable接口中的run()无返回值。

    创建了实现Callable接口的类之后,要使用new FutureTask(Callable<V> callable),创建一个FutureTask对象,然后执行get()方法来获取结果。

    FutureTask类实现了RunnableFuture接口,相当于使用Runnable执行任务,使用Future获取结果

  4. 使用线程池创建线程

    线程池,起始就是个容纳多个线程的容器,其中的线程可以重复使用,省去了频繁创建线程对象的操作,因为反复创建线程是非常消耗资源的。

    优点:实现自动化装配,易于管理,循环利用资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 使用线程池
    ExecutorService pool = Executors.newFixedThreadPool(2);
    pool.submit(new RunnableThread());
    Future submit = pool.submit(new Call());
    pool.submit(new Thread(){
    @Override
    public void run(){
    System.out.println("这里使用的匿名内部类创建的Thread类。");
    }
    });
    //如果传入的是Callable可以使用submit方法返回的Future来获取结果
    System.out.println("submit.get() = " + submit.get());
    pool.shutdown();

多线程的几种特性

  1. 多线程的随机性

    系统的抢占性调度,在多个线程之间随机执行。

  2. 多线程的可见性

    线程在执行的时候,会从方法区取得对应的值,如果其他线程对某个值进行修改,该线程就无法得到修改后的数值。(因为一直在使用的是该数值的缓存数值)只有在系统有反应时间的时候才能更新这些值。下面是让系统更新属性值的办法:

    1. Thread.sleep(1);
    2. 执行同步代码块时。比如synchronized(this){}
    3. Thread.currentThread.resume();//刷新这个线程

    虽然上面的方法都可以达到更新属性值的作用,但是都不是专门用来干这个的。所以我们可以在要共享读取的属性上添加volatile关键字。这样在系统要使用该属性的时候,都会去堆内存或方法区拿取该数据的新副本。

    volatile只能用在堆内存和方法区(元空间)中存储的属性。

  3. 多线程的有序性

    指令重排:在生成字节码的时候会为了代码的执行性能而进行不影响逻辑的指令重排(比如将统一变量的计算过程放在一起)

    因为指令重排的存在,在单线程的时候是一定不会出现问题的。但是在多线程的时候有一定几率会造成执行逻辑的混乱,需要注意。

  4. 多线程的原子性

    应该是最小的单元,不可再分割。

    • 针对数据的修改 - 原子性的解决方案 - 原子类

      数据类型 原子类
      int AtomicInteger
      long AtomicLong
      boolean AtomicBoolean

      引用类型可以使用AtomicReference

    • 可以使用synchronized关键字来实现

synchronized的三种使用方式

  1. 修饰普通方法

    给当前对象的实例加锁,进入同步代码前要获得 当前对象实例的锁

  2. 修饰静态方法

    给当前对象的类加锁,进入同步代码前要获得 当前对象类的锁 (.class)

  3. 修饰代码块

    可以传入对象或者一个类,表示在进入同步代码块前要获得传入的对象实例或者对象的类

    1
    2
    3
    synchronized (this){
    //要执行的代码块
    }

Lock和synchronized的区别

  • Lock是接口

    Lock需要手动的获取锁、释放锁

    线程较多时,Lock性能好

  • synchronized是关键字

    自动获取锁和释放锁

    线程少时,synchronized性能好

Lock接口比synchronized块的优势是什么

JDK1.5及以前synchronized关键字的效率太差。1.6之后有了非常大的提升。

Lock接口最大的优势在于分别为读和写提供了锁。

  • 读读共享
  • 写写互斥
  • 读写互斥
  • 写读互斥

乐观锁和悲观锁

  • 悲观锁

    认为每一次执行时都会有人抢占资源,所以每一次执行都要保证线程安全。(非常的悲观,所以叫悲观锁)

    悲观锁非常安全,但是性能上可能会差一些,用于写大于读的场景。

    悲观锁例子

    将Mysql中的自动提交(autocommit)关闭后Mysql中使用 select … for update 将数据锁住,直到commit后才将该条数据解锁。这个是非常典型的悲观锁策略。

  • 乐观锁

    认为数据一般情况下不会造成冲突,只有在数据提交更新的时候才会对数据是否冲突进行校验,如果发生冲突,返回错误信息。

    乐观锁性能上比悲观锁好,用于读大于写的场景,可以提高程序的吞吐量。

    乐观锁例子

    最典型的例子就是CAS(Compare and Swap 比较并交换)

    Java中的原子类()就是使用的CAS > compareAndSwapInt()

    1
    2
    3
    4
    5
    6
    7
    8
    public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    //while中的compareAndSwapInt()就是比较并交换,返回比较的结果,如果相等替换值后返回true
    return var5;
    }

简单的说,悲观锁就是进入即加锁,保证只有同一用户执行;乐观锁是提交数据时校验数据是否正确。

乐观锁:只针对数据的修改,只在修改处加校验,其他具体大段的逻辑他不管。

悲观锁:针对大段的上下文关联的逻辑。要把大段的代码变成原子性的。

final关键字

  • 修饰类:表示类不可被继承
  • 修饰方法:表示类不可被子类覆盖(但是可以重载)
  • 修饰变量:表示变量一旦被赋值就不可以更改它的值
    • 如果是基本数据类型,一旦初始化之后便不能进行修改
    • 如果是引用数据类型,一旦初始化之后便不能让其指向另外一个对象,但是引用对象的值是可以修改的。

Java类加载器

JDK自带有三个类加载器:

  • BootStrap ClassLoader

    ExtClassLoader的父类加载器,负责加载%JAVA_HOME%/lib目录下的jar包和class文件

  • ExtClassLoader

    AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext目录下的jar包和class文件

  • AppClassLoader

    自定义类加载器的父类加载器,负责加载classpath下的类文件,也是系统类加载器,线程上下文加载器。

    继承ClassLoader实现自定义类加载器

Java类加载器的双亲委派模型

向上委派:实际上就是查找缓存,是否加载该类,有则直接返回,没有继续往上

向下查找:查找加载路径,有则加载返回,没有则继续向下查找

双亲委派模型

向上委派到顶层加载器为止,向下查找到发起加载的加载器为止。

双亲委派模型的好处:

  • 主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如String。
  • 同时也避免了类的重复加载,因为JVM中区分不同的类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。

Java内存区域

image-20210307212216609

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法去
  • 直接内存(非运行时数据区的一部分)

Java堆事垃圾收集器管理的主要区域,因此也被称作GC堆。从垃圾回收的角度,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代(Eden,Survivor(From ,to))和老年代。

在新生代垃圾回收后,如果对象还存活,就从Eden转移到survivor,survivor清理15次后还存活,就会被转移到老年代中。

GC如何判断对象可以被回收

  • 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

    引用计数法可能会出现A引用了B,B又引用了A,这时候就算他们都不再使用了,但因为相互引用,计数器 = 1,永远无法被回收。

  • 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径成为引用链。当一个对象到GC Roots没用任何引用链时,则证明此对象时不可用的,那么虚拟机就判断是可回收对象。

GC Roots的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象 - 方法中的属性
  • 方法区中类静态属性引用的对象 - static
  • 方法区中常量引用的对象 - static
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象 - Native方法

两次可达性分析后都不可达,就会被GC回收

TCP的三次握手和4次挥手

image-20210307214226670

image-20210307214236204

SYN:**Synchronize Sequence Numbers **同步序列编号

ACK:Acknowledge character,即确认字符

FIN: 结束标记

MyISAM和InnoDB的区别

MyISAM时MySQL5.5版以前使用的默认数据库引擎。虽然性能极佳,而且提供了大量的性能,包括全文索引、压缩、空间函数。但是MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复

两者的对比:

  1. 是否支持行级锁:MyISAM只有表级锁,而InnoDB支持行级锁和表级锁,默认为行级锁。
  2. 是否支持事务和崩溃后的安全恢复:MyISAM强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是InnoDB提供事务支持,外部键等高级数据库功能。具有事务、回滚和崩溃修复能力和事务安全型表。
  3. 是否支持外键:MyISAM不支持,而InnoDB支持。
  4. 是否支持MVCC:仅InnoDB支持。应对高并发事务,MVCC值单纯的枷锁更高效。MVCC只在READ COMMITTEDREPEATABLE READ两个隔离级别下工作。MVCC可以使用乐观锁和悲观锁来实现,各数据库中的MVCC实现并不同意。推荐阅读:MySQL-InnoDB-MVCC多版本并发控制

不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。

MySQL索引

  1. 普通索引
  2. 唯一索引
  3. 主键索引
  4. 组合索引
  5. 全文索引

MySQL索引使用的数据结构主要有BTree索引(B+树)哈希索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能在最快;其余大部分场景,建议选择BTree索引。

事务的四大特性

  1. 原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用。
  2. 一致性:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
  3. 隔离性:并发访问数据库时,一个用户的事务不被其他事务锁干扰,各并发事务之间数据库是独立的。
  4. 持久性:一个事务被提交之后。他对数据库中数据的改变时持久的,即使数据库发生故障也不应该对其有任何影响。

并发带来的问题

  • 脏读:数据被修改之前读到的数据
  • 幻读:数据增加之前读了一次,数据增加之后又读了一次
  • 不可重复读:数据修改之前读了一次,数据修改之后又读了一次
  • 丢失修改:在修改后又被其他事务修改,本次修改丢失

四种隔离级别

  1. 读未提交
  2. 读已提交(Oracle默认)
  3. 可重复读(Mysql默认)
  4. 串行化

Spring的事务传播机制

A在调用B方法时的情况

Required (Spring默认):A没有事务,B自己新建一个事务。如果A有事务,B就加入A的事务。

Supports:A有事务B就加入,没有事务B直接以非事务方法执行

Mandatory:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常

Requires_new:创建一个新事物,如果存在当前事务,则挂起该事务

Not_Suppoorted:以非事务方式执行,如果当前存在事务,则挂起当前事务

Never:不使用事务,如果当前事务存在,则抛出异常

Nested:如果当前事务存在,则在嵌套事务中执行,否则就开启一个新的事务

SpringMVC的工作流程

  1. 用户发送请求到前端控制器 DispatcherServlet(拦截用户请求)
  2. DispatcherServlet将请求传递到HandlerMapping处理器映射器(通过URL找到注册方法)
  3. 找到具体的处理器后,生成处理器(以及处理器拦截器),并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器
  5. HandlerAdapter经过适配(根据方法找到参数)后通过反射调用具体的处理器(Controller)
  6. Controller执行完成返回ModelAndView给HandlerAdapter
  7. HandlerAdapter把ModelAndView传回DispatcherServlet,然后DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
  8. ViewReslover解析后返回具体View
  9. DispatcherServlet根据View进行数据填充
  10. DispatcherServlet将结果相应给用户