昨天知道得力集团在某一个培训机构进行校园招聘。于是我今天就来了,听了一下宣讲内容。发现得力集团刚
8月份在武汉成立了研究院,主要是做云服务,从0开始,现在的团队规模大概在20多人。一开始宣讲的是HR,后来就是技术总监,感觉技术总监给人一种很厉害的感觉。 不过薪资的确是太低了,4.5K - 5.5K,而且浮动的1k还要看学历。得力主要诱惑我的是云服务项目是从0开始,能让自己得到很大的提高。我的确心动了一下,心想先去面试一下,看自己的技术怎么样。

persistence.jpg
HR小姐姐。首先让我自我介绍一下,然后问了我以下的几个问题。 我回答的很干净利落,然后进入了复试。复试面试官是一个HR和技术总监,很让我意外的是技术总监问的题目把我问懵逼了,我都无法完整的答上来。
面试的最后,技术总监问我有什么想说的吗,我就咨询了加薪的标准,然后HR顿时脸黑了,很不耐烦的跟我说一堆。我从她的话知道了,涨薪极小值是10%,极大值是30%。我顿时感觉无望了,考核标准还是一年,而且实习没有薪水。最后HR还问我挂科没有,我说挂了单片机。
HR一听脸又黑了,不耐烦的噼里啪啦的说了一堆。我现在对得力集团完全没有好感了,但是技术总监难倒我的问题,我还是需要复盘分析一波,毕竟学习是自己的。
这个模式我很熟悉,EventBus的实现就是基于这个模式。但是还是有必要的提起这个模式。
- 抽象主题角色:把所有对观察者对象的引用保存在一个集合中,一般用`ArrayList`。每个抽象主题角色都可以有任意数量的观察者。抽象主题可以提供一个接口,可以增加和删除观察者。一般用一个抽象类和接口来实现。
- 抽象观察者角色:为所有具体的观察者定义一个接口,在得到主题的通知时可以更新自己。
- 具体主题角色: 在具体主题内部状态发生改变的时候,给所有注册过的观察者发出通知。
- 具体观察者角色: 实现抽象观察者中的更新接口,以便使本身的状态与主题的状态相互协调。手写观察者模式- 定义一个`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);
}
}
image.png
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。缺点:
- 如果一个被观察者对象有很多的直接和间接的观察者的话, 将所有的观察者都通知到会花费很多时间。
- 如果在观察者和被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,可能会导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。面试的时候,回答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注解,可以看我之前的一篇文章有提到过。MyBatis-Spring官方文档 学习笔记<context:component-scan base-package="com.augmentum.exam" />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应用以减轻数据库负载)缓存对象数据。
图片来自互联网.png
写着写着,突然又想到了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对象不是同一个对象。那么得出结论:反序列会重新生成一个对象。
image.png
Externalizable方式反序列化会调用无参构造器。我们去掉Person类的无参构造器,再运行一下,会发生什么呢?会打印出"no valid constructor"这一行,很显然需要一个无参构造器。
image.png
关于对象序列化,还有几点需要注意。
transient实例变量(瞬态实列变量)都不会被序列化。Serializable接口的类如果需要让某一个实例变量不被序列化,则可以在该实例变量前加transient修饰符,而不是加static关键字。虽然static关键字也可以达到这种效果,但是不能这样用。class文件。Serializable反序列化机制在恢复Java对象时无需调用构造器来初始化Java对象,而Externalizable反序列化机制就需要无参构造器。在这里还需要说,Java序列化机制采用一种特殊的序列化算法,如下:
关于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依然不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某一个位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的位置读入到缓冲区,等把需要插入的数据写入文件中后,再把缓冲区的内容追加到文件后面。心之所向,素履以往。生如逆旅,一苇以航。总有一天,已百炼,遂成钢。