小迈科技一面

简单三分钟自我介绍

自我介绍这里一笔带过,给对面介绍自己内在 + 外在 + 校园经历 + 校园项目 + 意向岗位

一面

1. Java基础

1.1 序列化和反序列化

首先了解一下序列化和反序列化的概念

序列化:将Java对象以二进制即字节码的形式保存在磁盘文件中,可以说是保存Java对象状态的过程,序列化可以实现对象保存的持久化;

反序列化:将保存在磁盘文件中的Java字节码重新转换为Java对象的过程;

其他特点:一般RPC框架底层协议通信就是通过序列化和反序列化在网络上传输Java对象。

序列化和反序列化的实现主要有两种,准确来说有三种方法:

采用默认的序列化方式,即通过ObjectOutPutStream类的writeObject(OutputStream out)方法来序列化到输出流中

输出流可以选择文件流、也可以选择管道流,甚至是二进制流

文件流是直接写到文件中再读取转成对象,管道流是通过管道缓存数据,然后再通过输入流连接管道读取转成对象

序列化的对象必须实现Seriablized接口,才能完成序列化和反序列化操作

1
2
3
4
class Student implements Serializable {
private String name;
private Integer age;
}
  • 第一种实现(采用默认)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//FileOutputStream fos = new FileOutputStream("D:\\student.txt");
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
pis.connect(pos);
ObjectOutputStream oos = new ObjectOutputStream(pos);

Student student = new Student();
student.setAge(21);
student.setName("zs");
oos.writeObject(student);

//FileInputStream fis = new FileInputStream("D:\\student.txt");
ObjectInputStream ois = new ObjectInputStream(pis);
Student res = (Student) ois.readObject();
System.out.println(res);
  • 第二种实现(自定义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义协议 比如我可以将加入对象头 cafe babe
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("自定义反序列化");
int magic = in.readInt();
this.name = (String) in.readObject();
this.age = (Integer) in.readObject();
}

// 自定义协议 比如我可以将加入对象头 cafe babe
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("自定义序列化");
// 4 字节 魔数
out.writeInt(0xCAFEBABE);
out.writeObject(this.name);
out.writeObject(this.age);
}
  • 第三种(自定义)

实现接口Externalizable并重写方法,其实跟第二种差别不大,只是第二种有默认的私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("实现 Externalizable 接口的自定义序列化");
out.writeObject(this.name);
out.writeObject(this.age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("实现 Externalizable 接口的自定义反序列化");
this.name = (String) in.readObject();
this.age = (Integer) in.readObject();
}

解决反序列化破坏单例

  • 在单例模式中,序列化可以破坏单例,这是需要重写readResolve() 方法,将之前的单例实例对象返回即可保证单例。

解决反射破坏单例

反射创建对象根本也是要调用构造方法,而且可以无视构造方法的访问修饰符(publicprivate

  • Java规定反射不能破坏枚举类型,采用枚举构造单例
  • 构造方法执行前,单例实例逻辑上不应该创建而且只执行一次,可判断是否已创建来避免反射创建多个实例,前提单例已先于反射

1.2 说一说数据类型有哪些

Java有八种基本数据类型 + String

1个位的bit,布尔类型的boolean,2个字节的short,4个字节的intchar,8个字节的longdoublefloat

其中基本类型中的包装类型常用的有Integer、Long

其中Integer会自动进行拆封装处理,也就是可以直接跟int类型比较数值上的大小

底层在-128-127之间会做缓存,在这之间通过Integer.valueOf(int)创建的对象都是同一个,使用了享元设计模式

1
2
3
System.out.println(new Integer(1) == 1);
System.out.println(Integer.valueOf(1) == Integer.valueOf(1));
System.out.println(Integer.valueOf(128) == Integer.valueOf(128));

结果

1
2
3
true
true
false

1.3 数据结构

给你你个浏览器,要求设计前进和后退的数据结构,优先考虑性能

那么由于浏览器前进后退访问,是一种FIFO的结构,比如你连续点击前进几个页面,最先进的页面最后返回,即LIFO

那么可以考虑栈的结构设计,栈结构设计要考虑性能,首先我们可以分析到,浏览器页面跳转没有涉及到页面的修改,即用于查询

那么优先考虑数组而不考虑链表

我们可以设计两个栈数组,一个用于入栈已前进的页面,一个用于入栈已后退的页面,取其中一个栈顶元素作为当前页面

2. MySQL

2.1 索引

在MySQL中,直接影响索引类型的是数据库的存储引擎

使用MyIsam存储引擎,数据文件和索引是分开的,索引会另外存储在另一个文件中

使用InnoDB存储引擎,数据和索引都存储在同一个文件中,而且是以B+数的数据结构存储,非叶子节点存储索引,叶子节点存储索引和索引对应的数据

就MyIsam引擎来说,索引中最主要的是聚簇索引,也就是主键的默认索引

单值索引和多值索引,这里是指组合索引,好的组合索引可以达到覆盖索引,可以做到避免回表,这里是因为叶子节点通过覆盖索引带了数据,因为B+树只有叶子节点带有数据,非叶子节点都是索引

而多个组合索引下,有效索引要做到左匹配,也就是必须从左顺序匹配到右查询,否则索引将失效

2.2 口述 sql

给你一张表,有三个字段,id、产品id、备注信息,现在需要你查询相同产品id的记录,然后id值相同的记录数大于等于5的产品id

首先呢,我们可以先定义这张表为S

因为涉及相同字段的记录,可以考虑直接分组,使用聚合函数

1
2
3
select pro_id from S
group by pro_id
having count(pro_id) >= 5

3. JVM

3.1 线程的死锁了解过吗?

比如有两个线程t1和t2,t1线程有资源r1,t2有资源r2,t1线程执行代码中需要资源r2,不过这段代码需要t2线程把资源r2释放才能执行,此时t2线程也执行代码中,释放资源前需要获取资源r1,但t2又需要r2,此时处于相互等待的状态,就导致了死锁。

从Java角度来说,资源相当于Java的锁对象,是互斥的,一个线程获取锁对象后,另一个线程只能等待,t1线程获取锁对象r1,也就是占有了锁r1,这时线程t2也是占有了锁对象r2,而线程t1需要获取锁对象r2才能往下执行代码,使得线程t1阻塞,而线程t2处于阻塞等待锁对象r1释放,才能释放锁对象r2,这样就导致死锁。(互相等待)

4. JUC

4.1 线程池有了解过吗?

线程池主要有核心线程数、救急线程和队列,队列分为阻塞队列和非阻塞队列。

目前线程池主要可以分为几类:

只有核心线程、无救急线程的线程池,此时等待队列中有要执行的任务,而核心线程在轮询地执行等待队列中的任务,如果队列满或者队列是无界队列,可能导致内存溢出问题。

一般这种线程池的做法就是使用了拒绝策略,拒绝策略可分为直接丢弃新任务、异常抛出(主动逻辑处理)、丢弃等待队列头结点、提交任务线程执行。

最后一个是推荐使用的

  • 不会造成数据丢失,也就不会出现业务损失;
  • 提交任务的线程被占用,新的任务不可提交,减缓任务提交的速度,相当于负反馈,能给到线程池一定的缓冲期;

只有救急线程的线程池,这种的话有线程池newCachedThreadPool,它能无限创建救急线程,队列采用SynchronousQueue ,是一个没有容量的队列,只有线程取任务时才能提交任务;

最后是一种核心线程数只有1的线程池,没有救急线程,任务队列无界,一般作为单线程任务,这样就不会有CPU的轮询切换,任务的执行效率最高,不过请求数太多的情况下,也是容易导致内存溢出。

非阻塞队列实现的有CurrentLinkedQueue,它是通过CAS无锁化机制的线程池队列,每个线程通过for(;;)执行,是一个单向且通过GC自动回收出队节点的,利用可达性算法分析,将next指向自己,即可触发延迟回收,利用元素的不可重用性,规避ABA问题

5. Linux

给你一个日志文件,要求你查询最近一天内中,匹配到关键字的所有记录数据

一般日志实时查询可以使用该命令,它会实时更新,而且会处于fg模式,tail打印文件末尾记录,也就是最近

1
tail -100f catalina.log | grep "关键字" 

而使用cat是查询历史,只能打印日志到屏幕

比如按照tailhead来查询关键字为20:的后5条数据并显示行号

1
cat -n log.log4j | grep "20:" | tail -n 5

时间段查询

1
grep '2022-08-21 20:1[1-9]' log.log4j