【Java】几道常见的秋招面试题

前言

只有光头才能变强html

Redis目前还在看,今天来分享一下我在秋招看过(遇到)的一些面试题(相对比较常见的)java

0、final关键字

简要说一下final关键字,final能够用来修饰什么?mysql

这题我是在真实的面试中遇到的,当时答得不太好,如今来整理一下吧。面试

final能够修饰类、方法、成员变量redis

  • 当final修饰类的时候,说明该类不能被继承
  • 当final修饰方法的时候,说明该方法不能被重写
    • 在早期,可能使用final修饰的方法,编译器针对这些方法的全部调用都转成内嵌调用,这样提升效率(但到如今通常咱们不会去管这事了,编译器和JVM都愈来愈聪明了)
  • 当final修饰成员变量时,有两种状况:
    • 若是修饰的是基本类型,说明这个变量的所表明数值永不能变(不能从新赋值)!
    • 若是修饰的是引用类型,该变量所的引用不能变,但引用所表明的对象内容是可变的!

值得一说的是:并非被final修饰的成员变量就必定是编译期常量了。好比说咱们能够写出这样的代码:private final int java3y = new Randon().nextInt(20);算法

你有没有这样的编程经验,在编译器写代码时,某个场景下必定要将变量声明为final,不然会出现编译不经过的状况。为何要这样设计?sql

在编写匿名内部类的时候就可能会出现这种状况,匿名内部类可能会使用到的变量:数据库

  • 外部类实例变量
  • 方法或做用域内的局部变量
  • 方法的参数
class Outer {


    // string:外部类的实例变量
    String string = "";


    //ch:方法的参数
    void outerTest(final char ch) {

        // integer:方法内局部变量
        final Integer integer = 1;
        new Inner() {
            void innerTest() {
                System.out.println(string);
                System.out.println(ch);
                System.out.println(integer);
            }
        };

    }
    public static void main(String[] args) {
        new Outer().outerTest(' ');
    }
    class Inner {
    }
}

复制代码

其中咱们能够看到:方法或做用域内的局部变量和方法参数都要显示使用final关键字来修饰(在jdk1.7下)!编程

若是切换到jdk1.8编译环境下,能够经过编译的~api

下面咱们首先来讲一下显示声明为final的缘由:为了保持内部外部数据一致性

  • Java只是实现了capture-by-value形式的闭包,也就是匿名函数内部会从新拷贝一份自由变量,而后函数外部和函数内部就有两份数据。
  • 要想实现内部外部数据一致性目的,只能要求两处变量不变。JDK8以前要求使用final修饰,JDK8聪明些了,可使用effectively final的方式

为何仅仅针对方法中的参数限制final,而访问外部类的属性就能够随意

内部类中是保存着一个指向外部类实例的引用,内部类访问外部类的成员变量都是经过这个引用。

  • 在内部类修改了这个引用的数据,外部类获取时拿到的数据是一致的!

那当你在匿名内部类里面尝试改变外部基本类型的变量的值的时候,或者改变外部引用变量的指向的时候,表面上看起来好像都成功了,但实际上并不会影响到外部的变量。因此,Java为了避免让本身看起来那么奇怪,才加了这个final的限制。

参考资料:

1、char和varchar的区别

  1. char是固定长度,varchar长度可变。varchar:若是原先存储的位置没法知足其存储的需求,就须要一些额外的操做,根据存储引擎的不一样,有的会采用拆分机制,有的采用分页机制
  2. char和varchar的存储字节由具体的字符集来决定(以前写错了);
  3. char是固定长度,长度不够的状况下,用空格代替。varchar表示的是实际长度的数据类型

选用考量:

  • 若是字段长度较和字符间长度相近甚至是相同的长度,会采用char字符类型

2、多个线程顺序打印问题

三个线程分别打印A,B,C,要求这三个线程一块儿运行,打印n次,输出形如“ABCABCABC....”的字符串。

原博主给出了4种方式,我认为信号量这种方式比较简单和容易理解,我这里粘贴一下(具体的可到原博主下学习)..

public class PrintABCUsingSemaphore {
    private int times;
    private Semaphore semaphoreA = new Semaphore(1);
    private Semaphore semaphoreB = new Semaphore(0);
    private Semaphore semaphoreC = new Semaphore(0);

    public PrintABCUsingSemaphore(int times) {
        this.times = times;
    }

    public static void main(String[] args) {
        PrintABCUsingSemaphore printABC = new PrintABCUsingSemaphore(10);

        // 非静态方法引用 x::toString 和() -> x.toString() 是等价的!
        new Thread(printABC::printA).start();
        new Thread(printABC::printB).start();
        new Thread(printABC::printC).start();

        /*new Thread(() -> printABC.printA()).start(); new Thread(() -> printABC.printB()).start(); new Thread(() -> printABC.printC()).start(); */
    }

    public void printA() {
        try {
            print("A", semaphoreA, semaphoreB);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void printB() {
        try {
            print("B", semaphoreB, semaphoreC);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void printC() {
        try {
            print("C", semaphoreC, semaphoreA);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void print(String name, Semaphore current, Semaphore next) throws InterruptedException {
        for (int i = 0; i < times; i++) {
            current.acquire();
            System.out.print(name);
            next.release();
        }
    }
}


复制代码

2018年9月14日18:15:36 yy笔试题就出了..

3、生产者和消费者

在很多的面经都能看到它的身影哈~~~基本都是要求可以手写代码的。

其实逻辑并不难,归纳起来就两句话:

  • 若是生产者的队列满了(while循环判断是否满),则等待。若是生产者的队列没满,则生产数据并唤醒消费者进行消费。
  • 若是消费者的队列空了(while循环判断是否空),则等待。若是消费者的队列没空,则消费数据并唤醒生产者进行生产。

基于原做者的代码,我修改了部分并给上我认为合适的注释(下面附上了原做者出处,感兴趣的同窗可到原文学习)

生产者:

import java.util.Random;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;

public class Producer implements Runnable {

    // true--->生产者一直执行,false--->停掉生产者
    private volatile boolean isRunning = true;

    // 公共资源
    private final Vector sharedQueue;

    // 公共资源的最大数量
    private final int SIZE;

    // 生产数据
    private static AtomicInteger count = new AtomicInteger();

    public Producer(Vector sharedQueue, int SIZE) {
        this.sharedQueue = sharedQueue;
        this.SIZE = SIZE;
    }

    @Override
    public void run() {
        int data;
        Random r = new Random();

        System.out.println("start producer id = " + Thread.currentThread().getId());
        try {
            while (isRunning) {
                // 模拟延迟
                Thread.sleep(r.nextInt(1000));

                // 当队列满时阻塞等待
                while (sharedQueue.size() == SIZE) {
                    synchronized (sharedQueue) {
                        System.out.println("Queue is full, producer " + Thread.currentThread().getId()
                                + " is waiting, size:" + sharedQueue.size());
                        sharedQueue.wait();
                    }
                }

                // 队列不满时持续创造新元素
                synchronized (sharedQueue) {
                    // 生产数据
                    data = count.incrementAndGet();
                    sharedQueue.add(data);

                    System.out.println("producer create data:" + data + ", size:" + sharedQueue.size());
                    sharedQueue.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupted();
        }
    }

    public void stop() {
        isRunning = false;
    }
}
复制代码

消费者:

import java.util.Random;
import java.util.Vector;

public class Consumer implements Runnable {

    // 公共资源
    private final Vector sharedQueue;

    public Consumer(Vector sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {

        Random r = new Random();

        System.out.println("start consumer id = " + Thread.currentThread().getId());
        try {
            while (true) {
                // 模拟延迟
                Thread.sleep(r.nextInt(1000));

                // 当队列空时阻塞等待
                while (sharedQueue.isEmpty()) {
                    synchronized (sharedQueue) {
                        System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
                                + " is waiting, size:" + sharedQueue.size());
                        sharedQueue.wait();
                    }
                }
                // 队列不空时持续消费元素
                synchronized (sharedQueue) {
                    System.out.println("consumer consume data:" + sharedQueue.remove(0) + ", size:" + sharedQueue.size());
                    sharedQueue.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}
复制代码

Main方法测试:

import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test2 {


    public static void main(String[] args) throws InterruptedException {

        // 1.构建内存缓冲区
        Vector sharedQueue = new Vector();
        int size = 4;

        // 2.创建线程池和线程
        ExecutorService service = Executors.newCachedThreadPool();
        Producer prodThread1 = new Producer(sharedQueue, size);
        Producer prodThread2 = new Producer(sharedQueue, size);
        Producer prodThread3 = new Producer(sharedQueue, size);
        Consumer consThread1 = new Consumer(sharedQueue);
        Consumer consThread2 = new Consumer(sharedQueue);
        Consumer consThread3 = new Consumer(sharedQueue);
        service.execute(prodThread1);
        service.execute(prodThread2);
        service.execute(prodThread3);
        service.execute(consThread1);
        service.execute(consThread2);
        service.execute(consThread3);

        // 3.睡一下子而后尝试中止生产者(结束循环)
        Thread.sleep(10 * 1000);
        prodThread1.stop();
        prodThread2.stop();
        prodThread3.stop();

        // 4.再睡一下子关闭线程池
        Thread.sleep(3000);

        // 5.shutdown()等待任务执行完才中断线程(由于消费者一直在运行的,因此会发现程序没法结束)
        service.shutdown();


    }
}
复制代码

另外,上面原文中也说了可使用阻塞队列来实现消费者和生产者。这就不用咱们手动去写wait/notify的代码了,会简单一丢丢。能够参考:

4、算法[1]

我如今须要实现一个栈,这个栈除了能够进行普通的push、pop操做之外,还能够进行getMin的操做,getMin方法被调用后,会返回当前栈的最小值,你会怎么作呢?你能够假设栈里面存的都是int整数

解决方案:

  • 使用一个min变量来记住最小值,每次push的时候,看看是否须要更新min。
    • 若是被pop出去的是min,第二次pop的时候,只能遍历一下栈内元素,从新找到最小值。
    • 总结:pop的时间复杂度是O(n),push是O(1),空间是O(1)
  • 使用辅助栈来存储最小值。若是当前要push的值比辅助栈的min值要小,那在辅助栈push的值是最小值
    • 总结:push和pop的时间复杂度都是O(1),空间是O(n)。典型以空间换时间的例子。
import java.util.ArrayList;
import java.util.List;

public class MinStack {

    private List<Integer> data = new ArrayList<Integer>();
    private List<Integer> mins = new ArrayList<Integer>();

    public void push(int num) {
        data.add(num);
        if (mins.size() == 0) {
            // 初始化mins
            mins.add(num);
        } else {
            // 辅助栈mins每次push当时最小值
            int min = getMin();
            if (num >= min) {
                mins.add(min);
            } else {
                mins.add(num);
            }
        }
    }

    public int pop() {
        // 栈空,异常,返回-1
        if (data.size() == 0) {
            return -1;
        }
        // pop时两栈同步pop
        mins.remove(mins.size() - 1);
        return data.remove(data.size() - 1);
    }

    public int getMin() {
        // 栈空,异常,返回-1
        if (mins.size() == 0) {
            return -1;
        }
        // 返回mins栈顶元素
        return mins.get(mins.size() - 1);
    }

}
复制代码

继续优化:

  • 栈为空的时候,返回-1极可能会带来歧义(万一人家push进去的值就有-1呢?),这边咱们可使用Java Exception来进行优化
  • 算法的空间优化:上面的代码咱们能够发现:data栈和mins栈的元素个数老是相等的,mins栈中存储几乎都是最小的值(此部分是重复的!)
    • 因此咱们能够这样作:当push的时候,若是比min栈的值要小的,才放进mins栈。同理,当pop的时候,若是pop的值是mins的最小值,mins才出栈,不然mins不出栈!
    • 上述作法能够必定避免mins辅助栈有相同的元素!

可是,若是一直push的值是最小值,那咱们的mins辅助栈仍是会有大量的重复元素,此时咱们可使用索引(mins辅助栈存储的是最小值索引,非具体的值)!

最终代码:

import java.util.ArrayList;
import java.util.List;


public class MinStack {

    private List<Integer> data = new ArrayList<Integer>();
    private List<Integer> mins = new ArrayList<Integer>();

    public void push(int num) throws Exception {
        data.add(num);
        if(mins.size() == 0) {
            // 初始化mins
            mins.add(0);
        } else {
            // 辅助栈mins push最小值的索引
            int min = getMin();
            if (num < min) {
                mins.add(data.size() - 1);
            }
        }
    }

    public int pop() throws Exception {
        // 栈空,抛出异常
        if(data.size() == 0) {
            throw new Exception("栈为空");
        }
        // pop时先获取索引
        int popIndex = data.size() - 1;
        // 获取mins栈顶元素,它是最小值索引
        int minIndex = mins.get(mins.size() - 1);
        // 若是pop出去的索引就是最小值索引,mins才出栈
        if(popIndex == minIndex) {
            mins.remove(mins.size() - 1);
        }
        return data.remove(data.size() - 1);
    }

    public int getMin() throws Exception {
        // 栈空,抛出异常
        if(data.size() == 0) {
            throw new Exception("栈为空");
        }
        // 获取mins栈顶元素,它是最小值索引
        int minIndex = mins.get(mins.size() - 1);
        return data.get(minIndex);
    }

}
复制代码

参考资料:

5、多线程下的HashMap

众所周知,HashMap不是一个线程安全的类。但有可能在面试的时候会被问到:若是在多线程环境下使用HashMap会有什么现象发生呢??

结论:

  • put()的时候致使的多线程数据不一致(丢失数据)
  • resize()操做会致使环形链表
    • jdk1.8已解决环链的问题(声明两对指针,维护两个连链表)
  • fail-fast机制,对当前HashMap同时进行删除/修改会抛出ConcurrentModificationException异常

参考资料:

6、Spring和Springboot区别

1、SpringBoot是可以建立出独立的Spring应用程序的

2、简化Spring配置

  • Spring因为其繁琐的配置,一度被人成为“配置地狱”,各类XML、Annotation配置,让人眼花缭乱,并且若是出错了也很难找出缘由。
  • Spring Boot项目就是为了解决配置繁琐的问题,最大化的实现convention over configuration(约定大于配置)。
    • 提供一系列的依赖包来把其它一些工做作成开箱即用其内置一个’Starter POM’,对项目构建进行了高度封装,最大化简化项目构建的配置。

3、嵌入式Tomcat,Jetty容器,无需部署WAR包

7、G1和CMS

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在如下方面表现的更出色:

  • G1是一个有整理内存过程的垃圾收集器,不会产生不少内存碎片
    • CMS采用的是标记清除垃圾回收算法,可能会产生很多的内存碎片
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户能够指按期望停顿时间

拓展阅读:

8、海量数据解决方案

海量数据的处理也是一个常常考的知识点,不管在面试仍是在笔试中都是比较常见的。有幸读了下面的文章,摘录了一些解决海量数据的思路:

  • Bloom filter布隆过滤器
    • 适用范围:能够用来实现数据字典,进行数据的判重,或者集合求交集
  • Hashing
    • 适用范围:快速查找,删除的基本数据结构,一般须要总数据量能够放入内存
  • bit-map
    • 适用范围:可进行数据的快速查找,判重,删除,通常来讲数据范围是int的10倍如下
    • 适用范围:海量数据前n大,而且n比较小,堆能够放入内存
  • 双层桶划分----其实本质上就是【分而治之】的思想,重在“分”的技巧上!
    • 适用范围:第k大,中位数,不重复或重复的数字
  • 数据库索引
    • 适用范围:大数据量的增删改查
  • 倒排索引(Inverted index)
    • 适用范围:搜索引擎,关键字查询
  • 外排序
    • 适用范围:大数据的排序,去重
  • trie树
    • 适用范围:数据量大,重复多,可是数据种类小能够放入内存
  • 分布式处理 mapreduce
    • 适用范围:数据量大,可是数据种类小能够放入内存

详细可参考原文:

9、幂等性

9.1HTTP幂等性

昨天去作了一套笔试题,经典的HTTP中get/post的区别。今天回来搜了一下,发现跟以前的理解有点出入

若是一我的一开始就作Web开发,极可能把HTML对HTTP协议的使用方式,当成HTTP协议的惟一的合理使用方式。从而犯了以偏概全的错误

单纯以HTTP协议规范来讲,可能咱们以前总结出的GET/POST区别就没用了。(但通读完整篇文章,我我的认为:若是面试中有GET/POST区别,仍是默认以Web开发场景下来回答较好,这也许是面试官想要的答案)

参考资料:


其中也学习到了幂等性这么一个概念,因而也作作笔记吧~~~

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

从定义上看,HTTP方法的幂等性是指一次和屡次请求某一个资源应该具备一样的反作用

  • 这里简单说一下“反作用”的意思:指当你发送完一个请求之后,网站上的资源状态没有发生修改,即认为这个请求是无反作用的

HTTP的GET/POST/DELETE/PUT方法幂等的状况:

  • GET是幂等的,无反作用
    • 好比我想要得到订单ID为2的订单:http://localhost/order/2,使用GET屡次获取,这个ID为2的订单(资源)是不会发生变化的!
  • DELETE/PUT是幂等的,有反作用
    • 好比我想要删除或者更新ID为2的订单:http://localhost/order/2,使用PUT/DELETE屡次请求,这个ID为2的订单(资源)只会发生一次变化(是有反作用的)!但继续屡次刷新请求,订单ID为2的最终状态都是一致的
  • POST是非幂等的,有反作用的
    • 好比我想要建立一个名称叫3y的订单:http://localhost/order,使用POST屡次请求,此时可能就会建立多个名称为3y的订单,这个订单(资源)是会屡次变化的,每次请求的资源状态都会变化

题外话:

HTTP协议自己是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不一样的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵照了HTTP协议的各类规定(充分利用了HTTP的方法);另外一种是SOA的,它并无彻底把HTTP当成应用层协议,而是把HTTP协议做为了传输层协议,而后在HTTP之上创建了本身的应用层协议

参考资料:

9.2接口幂等性

在查阅资料的时候,能够发现不少博客都讲了接口的幂等性。从上面咱们也能够看出,POST方法是非幂等的。但咱们能够经过一些手段来令POST方法的接口变成是幂等的。

说了那么多,那接口设计成幂等的好处是什么????

举个例子说一下非幂等的坏处:

  • 3y大一的时候是要抢体育课的,但学校的抢课系统作得贼烂(延迟很高)。我想要抢到课,就开了10多个Chrome标签页去抢(即便某个Chrome标签页崩了,我还有另外的Chrome标签页是可用的)。我想抢到乒乓球或者羽毛球。
  • 抢课时间一到,我就轮着点击我要想抢的乒乓球或者羽毛球。若是系统设计得很差,这个请求是非幂等的(或者说事务控制得很差),我手速足够快&&网络足够好,那我极可能抢到了屡次乒乓球或者羽毛球的课程了。(这是不合理的,一我的只能选一门课,而我抢到了多门或者屡次重复的课)
  • 涉及到商城的应用场景可能就是:用户下了多个重复的订单了

若是个人抢课接口是幂等的话,那就不会出现这个问题了。由于幂等是屡次请求某一个资源应该具备一样的反作用。

  • 在数据库后台最多只会有一条记录,不存在抢到多门课的现象了。

说白了,设计幂等性接口就是为了防止重复提交的(数据库出现多条重复的数据)!

网上有博主也分享了几条常看法决重复提交的方案:

  1. 同步锁(单线程,在集群可能会失效)
  2. 分布式锁如redis(实现复杂)
  3. 业务字段加惟一约束(简单)
  4. 令牌表+惟一约束(简单推荐)---->实现幂等接口的一种手段
  5. mysql的insert ignore或者on duplicate key update(简单)
  6. 共享锁+普通索引(简单)
  7. 利用MQ或者Redis扩展(排队)
  8. 其余方案如多版本控制MVCC 乐观锁 悲观锁 状态机等。。

参考资料:

最后

若是以上有理解错的地方,或者说有更好的理解方式,但愿你们不吝在评论区下留言。共同进步!

若是想看更多的原创技术文章,欢迎你们关注个人微信公众号:Java3y。公众号还有海量的视频资源哦,关注便可免费领取。

可能感兴趣的连接:

相关文章
相关标签/搜索