一个Java小白面试得力集团的收获

作者: cmazxiaoma | 来源:发表于2017-11-16 23:39 被阅读885次

    前言

    昨天知道得力集团在某一个培训机构进行校园招聘。于是我今天就来了,听了一下宣讲内容。发现得力集团刚8月份在武汉成立了研究院,主要是做云服务,从0开始,现在的团队规模大概在20多人。一开始宣讲的是HR,后来就是技术总监,感觉技术总监给人一种很厉害的感觉。

    不过薪资的确是太低了,4.5K - 5.5K,而且浮动的1k还要看学历。得力主要诱惑我的是云服务项目是从0开始,能让自己得到很大的提高。我的确心动了一下,心想先去面试一下,看自己的技术怎么样。

    persistence.jpg

    初面

    • 面试官是一个很漂亮的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);
        }
    }
    
    image.png
    • 观察者模式的优缺点:
      优点:

      • 观察者和被观察者是抽象耦合的。
      • 建立一套触发机制。

      缺点:

      • 如果一个被观察者对象有很多的直接和间接的观察者的话, 将所有的观察者都通知到会花费很多时间。

      • 如果在观察者和被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,可能会导致系统崩溃。

      • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。


    POST和GET的区别

    面试的时候,回答POSTGET的区别,受到了网上一些博客的误导。现在必须开始纠正了。

    • GETPOST本质上是TCP链接,并无差别。大多数浏览器通常都会限制url长度在2K个字节,而大多数服务器最多处理64K大小的url。由于HTTP的规定和浏览器/服务器的限制,导致它们在应用过程中体现出不同。

    • 对于GET方式的请求,浏览器会把http headerdata一起发送出去,服务器响应200(返回数据)。
      而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再去发送data,服务器响应200(返回数据)。所以GET产生一个TCP数据包,POST产生两个数据包。并不是所有浏览器都会在POST中发送两次包,而Firefox就只发送一次。

    • GET把参数包含在URL中,POST通过request body传递参数。

    • 幂等主要是为了处理同一个请求重复发送的情况,比如在请求响应前失去连接,如果方法是幂等的,就可以放心的重发一次请求。GETPUTDELETE都是幂等的,但是POST不是幂等,这也是浏览器再后退或者刷新时遇到POST请求会给用户提示的原因,重复请求可能会造成意想不到的结果。


    什么是幂等?

    • 幂等是一个数学或计算机学概念。常用于抽象代数中。对于单目运算符来说,如果一个运算对于在范围内的所有的一个数多次进行该运算所得的结果和进行一次运算所得的结果是一样的。那么我们就称该运算是幂等的。比如绝对值运算就是一个例子。在实数集中,有abs(a) = abs(abs(a))。对于双目运算,则要求当参与运算的两个值都是等值的情况下,如果满足运算结果与参与运算的两个值相等,那么可以称这个运算为幂等。比如max(x,x) = x

    • 幂等是指同一个请求方法执行多次和仅执行一次的效果完全相同。


    SpringMVC注解

    • 关于SpringMVC注解,可以看我之前的一篇文章有提到过。MyBatis-Spring官方文档 学习笔记

    • 面试官问我自动扫包的注解,但是我忘记怎么读了。<context:component-scan base-package="com.augmentum.exam" />


    Java序列化

    • 序列化就是把对象转换成字节序列的过程。

    • 反序列化就是把字节序列恢复为对象的过程。·

    • ParcelableSerializable都能实现序列化。SerializableJava中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量的I/O操作。而ParcelableAndroid中的序列化方式,因此更适合在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是Android推荐的序列化方式,因此我们要首选ParcelableParcelable主要用在内存序列化上,Serializable主要用于将对象序列化到存储设备中或者将对象序列化后通过网络传输。

    • 我们需要指定serialVersionUID的值,如果反序列化时当前的类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的hash值并把它赋值给serialVersionUID。这个时候当前类的serialVersionUID就和序列化的数据中的serialVersionUID不一致,于是反序列化失败了。

    面试官问我JavaSerializable序列化性能太差,问我如何高效的序列化。当时一脸懵逼,不知所云。现在回想起来,应该回答使用第三方序列化工具,也就是fastjson

    • 替换其他所有的json库,java世界里没有其他的json库能够和fastjson可相比了。

    • 使用fastjson的序列化和反序列化替换java Serializablejava 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?

    关于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又去访问该文件,就会出错,应该使用同步机制。比如银行的转账系统,数据库的保存操作等就需要同步了。

    那么NIOIO有什么区别呢

    • IO只能实现阻塞式的网络通信,NIO能够实现非阻塞的网络通信。

    • 标准IO基于字节或者字符流进行操作,而NIO是基于Channel进行操作的。

    • 流的读写通常是单向的,要么是输入,要么输出。
      通道是双向的,既可以写数据到Channel,又可以从Channel读取数据。

    区别说完了,那么开始NIO之旅了。

    • NIO使用了不同的方式来输入IONIO采用内存映射文件的方式去处理输入/输出,NIO将文件或者文件的一段区域映射到内存中,这样就可以向访问内存一样来访问文件了。

    • Channel与传统的InputStreamOutputStream最大的区别在于它提供了一个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装入数据结束后,调用Bufferflip()方法,该方法将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依然不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某一个位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的位置读入到缓冲区,等把需要插入的数据写入文件中后,再把缓冲区的内容追加到文件后面。

    参考文献


    注意事项


    尾言

    心之所向,素履以往。生如逆旅,一苇以航。总有一天,已百炼,遂成钢。

    相关文章

      网友评论

      本文标题:一个Java小白面试得力集团的收获

      本文链接:https://www.haomeiwen.com/subject/lyihvxtx.html