参加了一次美团笔试,收获颇多。

不同的输入方法

和力扣不一样,一些大厂的笔试包括面试时的算法题都是 ACM 模式,也就是需要自己处理输入,然后对输入处理并输出,而力扣是核心代码模式,不用你管输入,只需要实现指定方法,平台自会调用你的方法。在参加之前,我去卡码网上了解了一下,发现还是 Scanner 那一套,也就没在意,但当我真正开始做的时候感到有点不对,Scanner 恐怕不太够用。这篇博客主要讲一下 Java 里的输入流。

Java 的输入体系建立在 的概念之上,主要有两个抽象基类:InputStreamReader ,前者为字节流,处理原始字节数据,如图片、音频等;后者为字符流,处理字符数据,如文本文件、控制台输入等。两者之间通过 InputStreamReader 作为桥梁,将字节流转换为字符流。

InputStream 字节流

InputStream 类定义了所有字节输入流必须实现的核心办法。包括 int read() , int read(byte[] b) , void close()等方法。

Java 的 IO 体系采用了装饰器模式,意味着可以通过组合不同的流来实现复杂的功能。这里将 InputStream 的子类大致分为两类。

第一种,这种类直接与特定的数据源进行交互,比如 FileInputStream ByteArrayInputStream

第二种,这种类不直接连接数据源,而是通过继承装饰器基类 FilterInputStream 来包装一个已存在的 InputStream ,给它提供一些额外的功能,比如缓冲、数据类型转换等。这种类的典型代表有 BufferedInputStream DataInputStream 等。

Reader 字符流

Reader 类是字符输入流的抽象基类,定义的方法有 int read() , int read(char[] cbuf) , void close() 等。

它的子类和 InputStream 一样,也分两种,这里不再多说。但它有一个特别的子类: InputStreamReader 从它的类名就能看出来,它是字节流到字符流的单向桥梁,它可以读取字节,然后根据指定的字符编码将其解码为字符。

System.in

它是 System 类里的一个静态变量,类型是 InputStream ,所以它是一个字节流。如果需要按字符或行读取,通常需要对其包装,比如刚才提到的 InputStreamReader 和常见的 Scanner。通常情况下,System.in 默认连接到控制台(键盘),同时它读取数据也是阻塞式的,程序会一直等待,直到有数据可读或者流被关闭。

Scanner

Scanner 不属于 InputStreamReader ,它是一个独立的类。它的内部有这样一个变量 private Readable source; ,表示内部有一个 Readable 接口的实现,这就是关键所在,因为 Reader 就实现了 Readable 接口,所以, Scanner 可以接受 Reader , InputStream(通过桥转换) , String 作为输入源。这就是 new Scanner(System.in) 的用法由来。

这一点设计的十分巧妙, Scanner 并不依赖具体的类,不依赖 InputStream ,不依赖 Reader ,它只依赖能力,它只需要一个实现了 Readable 接口的对象。

下面是它的部分构造器

1
2
3
4
5
6
7
8
9
10
11
public Scanner(Readable source) {  
this(Objects.requireNonNull(source, "source"), WHITESPACE_PATTERN);
}

public Scanner(InputStream source) {
this(new InputStreamReader(source), WHITESPACE_PATTERN);
}

public Scanner(String source) {
this(new StringReader(source), WHITESPACE_PATTERN);
}

Scanner 内部使用正则表达式解析输入,提供了 nextInt() , nextLine() 等方法,极其方便。成也萧何,败也萧何,它会利用正则表达式来对输入类型进行检查,保证了安全性,但当数据量较大时,读取速度会显著下降,同时它的缓冲机制效率也相对较低,比不上 BufferedReader 等专为高效 IO 设计的类。

我进行了一个小实验,准备了一个文件,里面的内容是100万行数字1,分别用 ScannerBufferedReader 对文件读取,耗时相差十倍左右。

还有一点,Scanner 是在 Java.util 包里,而前面提到的输入流都是在 java.io 包里。

总结

分析了不同输入方法,我总结了一下。一般就两种情况,第一种:输入量极小,一般都是第一题,这时用 Scanner 完全没问题;第二种:输入量在 10 的六次方左右,或者说有很多组数据。一般都是下面这种写法,当一行里面有多个数据时,可以使用 StringTokenizer 来分词。

1
2
3
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); 
int k = Integer.parseInt(st.nextToken());
int q = Integer.parseInt(st.nextToken());

除了上面两种,还有更快的,就是自定义一个 FastScanner ,类里面使用 BufferedInputStream 直接处理字节流输入,同时需要自己写方法解析输入。

上面讲的全是输入流,其实输出流的设计和输入流简直一模一样,InputStream 对应 OutputStreamReader 对应 Writer ,输出流里也有一个桥:OuputStreamWriter,用来将字符流转换为字节流。System.in 也对应了 System.out ,同样,当需要输出很多行时,System.out.println 的性能也稍差,可以使用 StringBuilder 对输出拼接,最后转为字符串。

AI面试记录

记录一下问到的问题:

Redis 两种持久化机制的优缺点。

Redis 提供两种持久化机制

  • RDB(快照):在指定时间间隔内将内存数据生成快照保存到磁盘。

    • 优点:文件小;恢复速度快;对性能影响小(可fork子进程)。适合冷备份,灾难恢复。
    • 缺点:可能丢失最后一次快照之后,崩溃之前这一部分的数据。
  • AOF(追加文件):记录每个写操作命令到日志文件,重启时重写命令。

    • 优点:最多丢失一秒的数据(默认),安全性高;可读性强,内容为纯文本格式;体积过大会自动重写。
    • 缺点:文件体积大;恢复速度慢;对写性能有一定影响。

Redis4.0 后支持混合持久化,AOF 重写期间,先以 RDB 格式将内存中的数据快照保存到 AOF 文件的开头,再将重写期间的命令以 AOF 格式追加到文件末尾。

栈和队列的区别,使用场景

栈先进后出,入栈出栈都在一端进行,栈天然支持递归和回溯。常用使用场景有:函数调用栈、表达式求值。

队列先进先出,入队出队在两端进行。常见使用场景有消息队列、任务调度(线程池的任务队列)、BFS。

泛型类型的擦除原理

在编译期,编译器会将泛型代码中的泛型参数 E、K 等替换为边界类型,如果没有边界统一替换为 Object。运行期,JVM 眼中不存在泛型类或泛型方法。关于泛型可以看Java泛型 | withdong02

这篇博客就到这里,错误不可避免,欢迎指正。我会持续更新,欢迎收藏我的网站