往年 java 秋招核心面试题总结 (含解析)

262 篇文章 5 订阅
129 篇文章 1 订阅
本文总结了Java面试中的关键知识点,包括final关键字的使用,如修饰类、方法和成员变量时的效果。讲解了final变量在不同情况下的约束及其在匿名内部类中的作用。此外,讨论了MySQL中的char与varchar的区别,多线程问题如打印序列和生产者-消费者模型,以及栈数据结构的实现。还涉及了HashMap在多线程环境下的问题,Spring与SpringBoot的区别,G1与CMS垃圾收集器的对比,以及海量数据处理的解决方案,如Bloom Filter和堆。最后,探讨了HTTP方法的幂等性和接口幂等性的设计原则及其重要性。
摘要由CSDN通过智能技术生成

final 关键字

简要说一下 final 关键字,final 可以用来修饰什么?

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

final 可以修饰类、方法、成员变量.

  • 当 final 修饰类的时候,说明该类不能被继承

  • 当 final 修饰方法的时候,说明该方法不能被重写

  • 在早期,可能使用 final 修饰的方法,编译器针对这些方法的所有调用都转成内嵌调用,这样提高效率 (但到现在一般我们不会去管这事了,编译器和 JVM 都越来越聪明了)

  • 当 final 修饰成员变量时,有两种情况:

  • 如果修饰的是基本类型,说明这个变量的所代表数值永不能变 (不能重新赋值)!

  • 如果修饰的是引用类型,该变量所的引用不能变,但引用所代表的对象内容是可变的!

值得一说的是:并不是被 final 修饰的成员变量就一定是编译期常量了。

比如说我们可以写出这样的代码:

private final int java3y = new Randon().nextInt(20);

你有没有这样的编程经验,在编译器写代码时,某个场景下一定要将变量声明为 final,否则会出现编译不通过的情况。

为什么要这样设计?

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

  • 外部类实例变量

  • 方法或作用域内的局部变量

  • 方法的参数

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 编译环境下,可以通过编译的~

下面我们首先来说一下显示声明为 final 的原因:为了保持内部外部数据一致性.

  • Java 只是实现了 capture-by-value 形式的闭包,也就是匿名函数内部会重新拷贝一份自由变量,然后函数外部和函数内部就有两份数据。

  • 要想实现内部外部数据一致性目的,只能要求两处变量不变。JDK8 之前要求使用 final 修饰,JDK8 聪明些了,可以使用 effectively final 的方式

为什么仅仅针对方法中的参数限制 final,而访问外部类的属性就可以随意

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

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

那当你在匿名内部类里面尝试改变外部基本类型的变量的值的时候,或者改变外部引用变量的

指向的时候,表面上看起来好像都成功了,但实际上并不会影响到外部的变量。

所以,Java 为了不让自己看起来那么奇怪,才加了这个 final 的限制

一、char 和 varchar 的区别

  1. char 是固定长度,varchar 长度可变。varchar:如果原先存储的位置无法满足其存储的需求,就需要一些额外的操作,根据存储引擎的不同,有的会采用拆分机制,有的采用分页机制。

  2. char 和 varchar 的存储字节由具体的字符集来决定 (之前写错了);

  3. char 是固定长度,长度不够的情况下,用空格代替。varchar 表示的是实际长度的数据类型

选用考量:

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

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

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

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();
        }
    }
}

三、生产者和消费者

在不少的面经都能看到它的身影哈~~~基本都是要求能够手写代码的。

其实逻辑并不难,概括起来就两句话:

  • 如果生产者的队列满了 (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 的代码了,会简单一丢丢.

  • 使用阻塞队列解决生产者 - 消费者问题

四、算法 [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);
    }

}

五、多线程下的 HashMap

众所周知,HashMap 不是一个线程安全的类。

但有可能在面试的时候会被问到:如果在多线程环境下使用 HashMap 会有什么现象发呢??

结论:

  • put () 的时候导致的多线程数据不一致 (丢失数据)

  • resize () 操作会导致环形链表

  • jdk1.8 已解决环链的问题 (声明两对指针,维护两个连链表)

  • fail-fast 机制,对当前 HashMap 同时进行删除 / 修改会抛出 ConcurrentModificationException 异常

六、Spring 和 Springboot 区别

一、SpringBoot 是能够创建出独立的 Spring 应用程序的

二、简化 Spring 配置

  • Spring 由于其繁琐的配置,一度被人成为 “配置地狱”,各种 XML、Annotation 配置,让人眼花缭乱,而且如果出错了也很难找出原因。

  • Spring Boot 项目就是为了解决配置繁琐的问题,最大化的实现 convention over configuration (约定大于配置)。

  • 提供一系列的依赖包来把其它一些工作做成开箱即用其内置一个’Starter POM’,对项目构建进行了高度封装,最大化简化项目构建的配置。

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

七、G1 和 CMS

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

  • G1 是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。

  • CMS 采用的是标记清除垃圾回收算法,可能会产生不少的内存碎片

  • G1 的 Stop The World (STW) 更可控,G1 在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

八、海量数据解决方案

海量数据的处理也是一个经常考的知识点,无论在面试还是在笔试中都是比较常见的。

  • Bloom filter 布隆过滤器

  • 适用范围:可以用来实现数据字典,进行数据的判重,或者集合求交集

  • Hashing

  • 适用范围:快速查找,删除的基本数据结构,通常需要总数据量可以放入内存

  • bit-map

  • 适用范围:可进行数据的快速查找,判重,删除,一般来说数据范围是 int 的 10 倍以下

  • 适用范围:海量数据前 n 大,并且 n 比较小,堆可以放入内存

  • 双层桶划分 ---- 其实本质上就是【分而治之】的思想,重在 “分” 的技巧上!

  • 适用范围:第 k 大,中位数,不重复或重复的数字

  • 数据库索引

  • 适用范围:大数据量的增删改查

  • 倒排索引 (Inverted index)

  • 适用范围:搜索引擎,关键字查询

  • 外排序

  • 适用范围:大数据的排序,去重

  • trie 树

  • 适用范围:数据量大,重复多,但是数据种类小可以放入内存

  • 分布式处理 mapreduce

  • 适用范围:数据量大,但是数据种类小可以放入内存

九、幂等性

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 乐观锁 悲观锁 状态机等

除了分享上面的知识点外,还准备了一些大厂面试题,收集了各方面的,当前公司的,还有自己收集总结的,下面的图片截取的有pdf,有如果有需要的自取
各大公司面试题集合:

简历模板:
【这里想说,因为自己也走了很多弯路过来的,所以才下定决心整理,收集过程虽不易,但想到能帮助到一部分自学java 的人,心里也是甜的!有需要的伙伴请点㊦方】↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值