###前言
昨天知道得力集团在某一个培训机构进行校园招聘。于是我今天就来了,听了一下宣讲内容。发现得力集团刚
8
月份在武汉成立了研究院,主要是做云服务,从0
开始,现在的团队规模大概在20
多人。一开始宣讲的是HR
,后来就是技术总监,感觉技术总监给人一种很厉害的感觉。
不过薪资的确是太低了,
4.5K - 5.5K
,而且浮动的1k
还要看学历。得力主要诱惑我的是云服务项目是从0
开始,能让自己得到很大的提高。我的确心动了一下,心想先去面试一下,看自己的技术怎么样。
###初面
- 面试官是一个很漂亮的
HR
小姐姐。首先让我自我介绍一下,然后问了我以下的几个问题。- 你的职业规划是什么?
- 你是怎么学习的?
- 你觉得得力集团怎么样?
我回答的很干净利落,然后进入了复试。复试面试官是一个HR和技术总监,很让我意外的是技术总监问的题目把我问懵逼了,我都无法完整的答上来。
面试的最后,技术总监问我有什么想说的吗,我就咨询了加薪的标准,然后HR顿时脸黑了,很不耐烦的跟我说一堆。我从她的话知道了,涨薪极小值是10%
,极大值是30%
。我顿时感觉无望了,考核标准还是一年,而且实习没有薪水。最后HR
还问我挂科没有,我说挂了单片机。
HR
一听脸又黑了,不耐烦的噼里啪啦的说了一堆。我现在对得力集团完全没有好感了,但是技术总监难倒我的问题,我还是需要复盘分析一波,毕竟学习是自己的。
###关于复试的题目
####观察者模式 这个模式我很熟悉,EventBus
的实现就是基于这个模式。但是还是有必要的提起这个模式。
-
当对象存在一对多关系时,则使用观察者模式。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为模式。
-
用白话说,就是观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,让它们能够自动更新自己。
-
观察者模式的组成:
-
抽象主题角色:把所有对观察者对象的引用保存在一个集合中,一般用
ArrayList
。每个抽象主题角色都可以有任意数量的观察者。抽象主题可以提供一个接口,可以增加和删除观察者。一般用一个抽象类和接口来实现。 -
抽象观察者角色:为所有具体的观察者定义一个接口,在得到主题的通知时可以更新自己。
-
具体主题角色: 在具体主题内部状态发生改变的时候,给所有注册过的观察者发出通知。
-
具体观察者角色: 实现抽象观察者中的更新接口,以便使本身的状态与主题的状态相互协调。
-
-
手写观察者模式
Demo
。- 定义一个
Subject
类,也就是被观察者。
- 定义一个
public class Subject {
private List<Observer> observers = new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer) {
observers.add(observer);
}
public void notifyAllObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
复制代码
- 定义一个抽象的
ObServer
类,也就是抽象的观察者类。
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
复制代码
- 定义一个具体的
BinaryObserver
类,它继承ObServer
类。
public class BinaryObserver extends Observer {
public BinaryObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println("binary=" + Integer.toBinaryString(subject.getState()));
}
}
复制代码
- 定义一个具体的
OctalObserver
类,它继承于ObServer
类。
public class OctalObserver extends Observer {
public OctalObserver(Subject subject) {
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println("octal:" + Integer.toOctalString(subject.getState()));
}
}
复制代码
- 编写测试用例
ObserverPatternDemo
,并运行。
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new BinaryObserver(subject);
new OctalObserver(subject);
subject.setState(15);
subject.setState(10);
}
}
复制代码
-
观察者模式的优缺点: 优点:
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。
缺点:
-
如果一个被观察者对象有很多的直接和间接的观察者的话, 将所有的观察者都通知到会花费很多时间。
-
如果在观察者和被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,可能会导致系统崩溃。
-
观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
####POST和GET的区别 面试的时候,回答POST
和GET
的区别,受到了网上一些博客的误导。现在必须开始纠正了。
-
GET
和POST
本质上是TCP
链接,并无差别。大多数浏览器通常都会限制url
长度在2K
个字节,而大多数服务器最多处理64K
大小的url
。由于HTTP的规定和浏览器/服务器的限制,导致它们在应用过程中体现出不同。 -
对于
GET
方式的请求,浏览器会把http header
和data
一起发送出去,服务器响应200
(返回数据)。 而对于POST
,浏览器先发送header
,服务器响应100 continue
,浏览器再去发送data
,服务器响应200
(返回数据)。所以GET
产生一个TCP
数据包,POST
产生两个数据包。并不是所有浏览器都会在POST
中发送两次包,而Firefox
就只发送一次。 -
GET
把参数包含在URL
中,POST
通过request body
传递参数。 -
幂等主要是为了处理同一个请求重复发送的情况,比如在请求响应前失去连接,如果方法是幂等的,就可以放心的重发一次请求。
GET
,PUT
,DELETE
都是幂等的,但是POST
不是幂等,这也是浏览器再后退或者刷新时遇到POST
请求会给用户提示的原因,重复请求可能会造成意想不到的结果。
####什么是幂等?
-
幂等是一个数学或计算机学概念。常用于抽象代数中。对于单目运算符来说,如果一个运算对于在范围内的所有的一个数多次进行该运算所得的结果和进行一次运算所得的结果是一样的。那么我们就称该运算是幂等的。比如绝对值运算就是一个例子。在实数集中,有
abs(a) = abs(abs(a))
。对于双目运算,则要求当参与运算的两个值都是等值的情况下,如果满足运算结果与参与运算的两个值相等,那么可以称这个运算为幂等。比如max(x,x) = x
。 -
幂等是指同一个请求方法执行多次和仅执行一次的效果完全相同。
####SpringMVC注解
-
关于
SpringMVC
注解,可以看我之前的一篇文章有提到过。MyBatis-Spring官方文档 学习笔记 -
面试官问我自动扫包的注解,但是我忘记怎么读了。
<context:component-scan base-package="com.augmentum.exam" />
####Java序列化
-
序列化就是把对象转换成字节序列的过程。
-
反序列化就是把字节序列恢复为对象的过程。·
-
Parcelable
和Serializable
都能实现序列化。Serializable
是Java
中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量的I/O
操作。而Parcelable
是Android
中的序列化方式,因此更适合在Android
平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是Android
推荐的序列化方式,因此我们要首选Parcelable
。Parcelable
主要用在内存序列化上,Serializable
主要用于将对象序列化到存储设备中或者将对象序列化后通过网络传输。 -
我们需要指定
serialVersionUID
的值,如果反序列化时当前的类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的hash
值并把它赋值给serialVersionUID
。这个时候当前类的serialVersionUID
就和序列化的数据中的serialVersionUID
不一致,于是反序列化失败了。
面试官问我Java
的Serializable
序列化性能太差,问我如何高效的序列化。当时一脸懵逼,不知所云。现在回想起来,应该回答使用第三方序列化工具,也就是fastjson
。
-
替换其他所有的
json
库,java
世界里没有其他的json库能够和fastjson可相比了。 -
使用
fastjson
的序列化和反序列化替换java Serializable
,java Serializable
不单性能慢,而且体积大。 -
使用
fastjson
替换hessian
(是一个基于binary-RPC
实现的远程通讯library
,使用二进制传输数据),json
协议和hessian
协议大小差不多一样,而且fastjson
性能优越,10
倍于hessian
。 -
把
fastjson
用于memcached
(是一个高性能的分布式内存对象缓存对象系统,用于动态Web
应用以减轻数据库负载)缓存对象数据。
写着写着,突然又想到了Externalizable
接口。这是Java
提供的另一种序列化机制,这种序列化方式完全由程序员决定存储和恢复对象数据。要实现该目标,Java
类必须实现Externalizable
接口。我们接下来写一个Demo
。
- 定义一个
Person
类,实现了java.io.Externalizable
接口。Person
类必须去实现readExternal()
,writeExternal()
两个方法。
public class Person implements Externalizable {
private String name;
private int age;
public Person(String name, int age) {
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
public Person() {
System.out.println("无参数的构造器");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
this.age = in.readInt();
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
复制代码
- 接下来我们写一个测试用例。
public class ExternalizableDemo {
public static void main(String[] args) throws IOException {
File fileName = new File("externalizable.txt");
FileOutputStream fos = new FileOutputStream(fileName);
FileInputStream fis = new FileInputStream(fileName);
ObjectOutputStream os = new ObjectOutputStream(fos);
ObjectInputStream is = new ObjectInputStream(fis);
try {
Person person = new Person("cmazxiaoma", 21);
os.writeObject(person);
os.writeObject(person);
Person newPerson = (Person) is.readObject();
System.out.println(newPerson);
System.out.println("两个person对象引用是否相等 :" + person == newPerson + "");
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
close(is);
close(os);
close(fis);
close(fos);
}
}
public static void close(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
-
运行测试用例,看控制台输出。我们发现反序列化的时候,会读取
Java
对象中的数据,然后调用无参构造器给对象完成必要的初始化。我们还会发现序列化之前的Person
对象和反序列之后生成的Person
对象不是同一个对象。那么得出结论:反序列会重新生成一个对象。 -
那么可以有一个假设,使用
Externalizable
方式反序列化会调用无参构造器。我们去掉Person
类的无参构造器,再运行一下,会发生什么呢?会打印出"no valid constructor"
这一行,很显然需要一个无参构造器。
关于对象序列化,还有几点需要注意。
-
对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量,
transient
实例变量(瞬态实列变量)都不会被序列化。 -
实现
Serializable
接口的类如果需要让某一个实例变量不被序列化,则可以在该实例变量前加transient
修饰符,而不是加static
关键字。虽然static
关键字也可以达到这种效果,但是不能这样用。 -
反序列化对象时必须有序列化对象的
class
文件。 -
当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取。
-
Serializable
反序列化机制在恢复Java
对象时无需调用构造器来初始化Java
对象,而Externalizable
反序列化机制就需要无参构造器。
在这里还需要说,Java
序列化机制采用一种特殊的序列化算法,如下:
-
所有保存到磁盘中的对象都有一个序列化编号。
-
当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未在本次虚拟机中被序列化过,系统才会将该对象转换成字节序列并输出。
-
如果某个对象已经序列化过,程序将只输出一个序列化编号,而不是再次重新序列化该对象。
####什么是NIO? 关于NIO
这个概念,也是我学习Java
知识所忽略的一个点吧。以前看博客的时候,零星的看过,当时没有什么在意。记得昨天技术总监问我NIO
是什么? 我当时没听清他的回答,然后反问NIO
是什么? 他跟我说NIO
是异步IO
,也就是Asynchronous IO
的意思。当时一脸懵逼,不知所云。今天在掘金上面搜索了一下关于NIO
的文章,也总结一波。
-
BIO(Blocking I/O)
:同步阻塞IO
模式,数据的读取写入必须阻塞在一个线程内等待其完成。 -
NIO(New I/O)
:同时支持阻塞和非阻塞模式。我们以同步非阻塞IO
模式来说,如果拿烧开水为说,NIO
的做法是开启一个线程不断的轮询水壶的状态。 -
AIO(Asynchronous I/O)
:异步非阻塞IO
模式。异步非阻塞和同步非阻塞的区别在于无需开启一个线程去轮询水壶的状态,当水烧开了,水壶会发生叫声,系统就会通知对应的线程来处理。
那么我们需要说同步和异步的区别了。
-
同步:比如发送一个请求,需要等待返回,然后才能发送下一个请求,中间有等待过程。
-
异步:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。
-
场景需求: 同步可以避免读脏数据的发生。一般共享某一资源的时候用,如果每个人都有修改权限,当
A
删除了一个文件时,B
又去访问该文件,就会出错,应该使用同步机制。比如银行的转账系统,数据库的保存操作等就需要同步了。
那么NIO
与IO
有什么区别呢
-
IO
只能实现阻塞式的网络通信,NIO
能够实现非阻塞的网络通信。 -
标准
IO
基于字节或者字符流进行操作,而NIO
是基于Channel
进行操作的。 -
流的读写通常是单向的,要么是输入,要么输出。 通道是双向的,既可以写数据到
Channel
,又可以从Channel
读取数据。
区别说完了,那么开始NIO
之旅了。
-
NIO
使用了不同的方式来输入IO
,NIO
采用内存映射文件的方式去处理输入/输出,NIO
将文件或者文件的一段区域映射到内存中,这样就可以向访问内存一样来访问文件了。 -
Channel
与传统的InputStream
,OutputStream
最大的区别在于它提供了一个map()
方法,通过该map
方法可以直接将一块数据映射到内存中。如果说传统的输入/输出系统是面向流的处理,那么NIO
则是面向块的处理。 -
Buffer
可以理解成一个容器,它的本质是一个数组,发送到Channel
中的所有对象都必须先放到Buffer
中,而从Channel
中读取的数据也必须先放入Buffer
。 -
NIO
还提供了用于将Unicode
字符串映射成字节序列以及逆映射操作的Charset
类,也提供了非阻塞式输入/输出的Selector
类。
在Buffer中有3
个重要的概念: 容量(capacity)
,界限(limit)
,位置(position)
。
-
capacity
: 缓冲区的容量标识该Buffer
的最大数据容量。 -
limit
:位于limit
后的数据既不可被读,也不可被写。 -
position
:用于指明下一个可以被读写的缓冲区位置的索引(类似于IO
流中的记录指针)。
接着就来说Buffer
中的flip()
和clear()
方法。
-
当
Buffer
装入数据结束后,调用Buffer
的flip()
方法,该方法将limit
设置为position
位置,并将position
设为0
,这就使得Buffer
的读写指针又移动了开始位置。简而言之,filp()
为从Buffer
中取出数据做好准备。 -
当
Buffer
输出数据结束后,Buffer
调用clear()
方法,clear()
方法不是清空Buffer
中的数据,它仅仅将position
置为0
,将limit
设置为capacity
,这样为再次向Buffer
中装入数据做好准备。
理论总结的很多,那么开始手写代码吧。
- 我们在
NIODemo
中写了3
种方法,都是从读取"nio_read.txt"
文件的内容,然后写入"nio_write.txt"
文件中。
public class NIODemo {
public static void main(String[] args) throws IOException {
// methodOne();
// methodTwo();
methodThree();
}
public static void methodOne() throws IOException {
String rFile = "nio_read.txt";
String wFile = "nio_write.txt";
FileChannel rFileChannel = new FileInputStream(rFile).getChannel();
FileChannel wFileChannel = new FileOutputStream(wFile).getChannel();
ByteBuffer buff = ByteBuffer.allocate(1024);
while (rFileChannel.read(buff) > 0) {
buff.flip();
wFileChannel.write(buff);
buff.clear();
}
close(wFileChannel);
close(rFileChannel);
}
public static void methodTwo() throws IOException {
String rFile = "nio_read.txt";
String wFile = "nio_write.txt";
FileChannel rFileChannel = new FileInputStream(rFile).getChannel();
FileChannel wFileChannel = new FileOutputStream(wFile).getChannel();
rFileChannel.transferTo(0, rFileChannel.size(), wFileChannel);
close(wFileChannel);
close(rFileChannel);
}
public static void methodThree() throws IOException {
String rFile = "nio_read.txt";
String wFile = "nio_write.txt";
RandomAccessFile raf = new RandomAccessFile(rFile, "rw");
FileChannel randomChannel = raf.getChannel();
FileChannel wFileChannel = new FileOutputStream(wFile).getChannel();
// 将Channel中的所有数据映射成ByteChannel
ByteBuffer buff = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0, raf.length());
// 把Channel的指针移动到最后
randomChannel.position(raf.length());
wFileChannel.write(buff);
close(wFileChannel);
close(randomChannel);
}
public static void close(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
-
既然
methodThree()
方法中用到了RandomAccessFile
。那么就顺便说一下使用注意事项:RandomAccessFile
依然不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某一个位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的位置读入到缓冲区,等把需要插入的数据写入文件中后,再把缓冲区的内容追加到文件后面。 ###参考文献
###注意事项
-
如果第一个参考文献链接打开提示参数错误,那么请复制链接通过QQ或者微信打开https://mp.weixin.qq.com/s?sn=71f6c214f3833d9ca20b9f7dcd9d33e4&__biz=MzI3NzIzMzg3Mw%3D%3D&mid=100000054&idx=1#rd
-
如果最后一个参考文献链接打开提示参数错误,那么请复制链接通过QQ或者微信打开 https://mp.weixin.qq.com/s?__biz=MzIzMzgxOTQ5NA==&mid=100000199&idx=1&sn=1e9006f2289cdfb612f22e9f6b7b44cb&chksm=68fe9dce5f8914d8ba791b26ae6de6742686dc660db0f38a67c41ed87c942cbe679f26a4b24c#rd
###尾言
心之所向,素履以往。生如逆旅,一苇以航。总有一天,已百炼,遂成钢。