某飞2022秋招笔试知识点总结(超详细)

前言

相比起上午美团暴力的五到硬核编程题,今晚科大讯飞的笔试题可能更适合我这种小菜鸡,笔试做一个是少一个了,先从里面看自己的知识储备,也趁着这个热乎劲来增加一些知识储备,也把一些模棱两可的知识学熟练了,以下只是本人有印象的考点,可能不全,还请大家谅解。

相关知识点

Java8新特性的使用

我这题做的是一个业务模拟题,在集合里加一些菜名和对应卡路里,然后通过stream流的方式来筛选出大于300卡路里的菜品并分页打印,题目很亲切,符合真实开发场景。这里给大家看看相关知识点先,Java8已经出来很久了,还没用上jdk8的小伙伴也可以试试,在数据处理的时候很酸爽方便。在这里插入图片描述
Stream(流)是一个来自数据源的元素队列并支持聚合操作

  1. 元素是特定类型的对象,形成一个队列,Java中的Stream并不会存储元素,而是按需计算。
  2. 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
  3. 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  1. Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)
  2. 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代
    Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现

在 Java 8 中, 集合接口有两个方法来生成流:

  1. stream() − 为集合创建串行流
  2. parallelStream() − 为集合创建并行流

大家想了解更多的stream流常用方法可以参考一下这个博客 http://t.csdn.cn/S33k6

插入排序和希尔排序

插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想,参考下面代码

int[] insertSort(int[] arr){
    int n=arr.length;
    for(int i = 1; i<n;i++){//当前位置的数
        for(int j = i;j>0;j--){//a[i]与前面的数进行比较,进行升序排序
            if(arr[j]<arr[j-1]){//交换位置
				int temp = arr[j-1];
				arr[j-1] = arr[j];
				arr[j] = temp;
            }
        }
    }
    return arr;//返回排序后的数组
}//时间复杂度:O(N^2)

希尔排序
(大家也可以去看《数据结构与算法分析Java语言描述》第三版188页)
基本思想:希尔排序是把序列按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰好被分为一组,算法便终止
算法实现:希尔排序需要定义一个增量,这里选择增量为gap=length/2,缩小增量以gap=gap/2的方式,这个增量可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列,这个增量是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。

(1)对于一个无序序列{8,9,1,7,2,3,5,4,6,0}来说,我们初始增量为gap=length/2=5,所以这个序列要被分为5组,分别是{8,3},{9,5},{1,4},{7,6},{2,0},对这5组分别进行直接插入排序,则小的元素就被调换到了前面,然后再缩小增量gap=gap/2=2
在这里插入图片描述(2).上面缩小完增量后,序列再次被分为2组,分别是{3,1,0,9,7}和{5,6,8,4,2},再对这两组进行直接插入排序,那么序列就更加有序了
在这里插入图片描述
(3)然后再缩小增量gap=gap/2=1,这时整个序列就被分为一组即{0,2,1,4,3,5,7,6,9,8},最后再进行调整,就得到了有序序列{0,1,2,3,4,5,6,7,8,9}
在这里插入图片描述

#include <iostream>
#include <vector>

using namespace std;

vector<int> ShellSort(vector<int> list){
	vector<int> result = list;
	int n = result.size();
	for (int gap = n >> 1; gap > 0; gap >>= 1){
		for (int i = gap; i < n; i++){
			int temp = result[i];
			int j = i - gap;
			while (j >= 0 && result[j] > temp){
				result[j + gap] = result[j];
				j -= gap;
			}
			result[j + gap] = temp;
		}
		for (int i = 0; i < result.size(); i++){
			cout << result[i] << " ";
		}
		cout << endl;
	}
	return result;
}

void main(){
	int arr[] = { 6, 4, 8, 9, 2, 3, 1 };
	vector<int> test(arr, arr + sizeof(arr) / sizeof(arr[0]));
	cout << "排序前" << endl;
	for (int i = 0; i < test.size(); i++){
		cout << test[i] << " ";
	}
	cout << endl;
	vector<int> result;
	result = ShellSort(test);
	cout << "排序后" << endl;
	for (int i = 0; i < result.size(); i++){
		cout << result[i] << " ";
	}
	cout << endl;
	system("pause");
}

希尔排序算法分析
在这里插入图片描述
部分参考该博客http://t.csdn.cn/8Y54O

threadlocal

ThreadLocal简介

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。

但是ThreadLocal与synchronized有本质的区别:

Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本

,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

threadlocal的使用场景
我原来在一个外卖项目中也使用过threadlocal来保存用户的线程状态,因为每个用户的信息是唯一的,那个项目的用户校验又比较简陋,先用threadlocal保存用户的购物车信息等,让不同线程的用户隔离。

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 * @author William
 * @create 2022-04-22 14:22
 */
public class BaseContext {

    private static ThreadLocal<Long> threadLocal =new ThreadLocal<>();

    /**
    * 设置id
    *@Param [id]
    *@Return
    */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    /**
    * 获取id
    *@Param []
    *@Return
    */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}
//一点用到的地方
/**
     * 用户下单
     *@Param [orders]
     *@Return
     */
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户的id
        Long userId = BaseContext.getCurrentId();
        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId, userId);
        List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);
        if(shoppingCarts == null || shoppingCarts.size() == 0){
            throw new CustomException("购物车为空,不能下单");
        }
        //查询用户数据
        User user = userService.getById(userId);
        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook == null){
            throw new CustomException("用户地址信息有误,不能下单");
        }
        long orderId = IdWorker.getId();//订单号

        AtomicInteger amount = new AtomicInteger(0);

        List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());


        //向订单表插入数据,一条数据
        //orders.setNumber(String.valueOf(orderId));
        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);//代表待派送
        orders.setAmount(new BigDecimal(amount.get()));//订单总金额
        orders.setUserId(userId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress(addressBook.getProvinceName() == null ? " " : addressBook.getProvinceName()
                + (addressBook.getCityName() == null ? " " : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? " " : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? " " : addressBook.getDetail()));

        this.save(orders);
        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);
        //清空购物车数据
        shoppingCartService.remove(wrapper);
    }

总结一下Threadlocal的两个作用

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  2. 在任何方法中都可以轻松获取到该对象

顺便提一嘴threadlocal的内存泄漏问题

ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果
ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会
被清理掉。这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措
施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap 实现
中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null
的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法

static class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
	 Object value;
	 Entry(ThreadLocal<?> k, Object v) {
	 	super(k);
	 	value = v;
	 }
 }

关于弱引用:
如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。弱引⽤与软引⽤的区别在
于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存
区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只
具有弱引⽤的对象。
弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃
圾回收,Java 虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中

部分参考http://t.csdn.cn/s6pZE

kruskal

  1. 克鲁斯卡尔 (Kruskal) 算法,是用来求加权连通图的最小生成树的算法 。
  2. 基本思想 :按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
  3. 具体做法 :首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

图解:
在这里插入图片描述
更多可参考这篇博客http://t.csdn.cn/DdIMb

递归算法时间复杂度

那道题目呢直接给了一个递归算法表达式让我求时间复杂度,我所了解到的两个方法一个是根据递归树求解,一个是根据主定理求解,个人推荐主定理,较为方便在这里插入图片描述在这里插入图片描述

二级页表

二级页表是我才了解到的一个概念,这个也比较好理解,一本书的目录就是一级页表,那这个书太厚了呢,那我们如法炮制,将这个目录再编成一本书,就是二级页表了。

案例:400MB的游戏程序载入了内存
32位系统,块大小固定为4K
则低12位一定是对应块大小的,高20位用于页表索引
400MB/4KB = 100K条,100K条索引,但页表的100K条中,每条32位占4B,所以页表总共就占了4B100K=400KB的空间
那这400KB的页表也是要放在内存中的,内存一块是4KB,则实际在内存中要占用100块,或者叫100页
——————
但是100页有必要一次性装在进去吗?没必要
前面我们说,程序的数据是按需载入内存,不是一股脑全部塞进去,用不到的就先候着。而这100块或者叫100页其实对了400MB全部全部的数据。
正确的应该是先载入40MB数据,加载到某个地方还需要另外200M数据,再把200MB载入内存
而载入40MB的时候,需要载入总共100页中的10页,同理,再需要另外200MB时候,再对应载入50页。以此节省内存空间
——————
但是如何知道引入100块中的那几块呢?
有点递归的思想,把这100块页表数据看成另外某个程序的空间,也就是一个要占用400KB的程序。
400KB的程序,每块4KB,400KB/4K = 100条,100条索引每条32位占4B,所以页表(这里是页表的页表了)总共需要占用100
4B = 400B空间
400B作为页表也是要占空间的,一块是4KB,它只占了400B,也就是只占了一个最小单位 页的仅仅十分之一空间
——————
对比一下原来400MB的页表占用100页
现在400K(100页)的页表占用0.1页
好了,在极端情况下,32位操作系统内存最大4GB
4000MB的内存占满的情况下,页表占用1000页
4000KB(上面页表的1000页)的页表占用1页(刚刚好1页 4KB大小)
所以采用对页表再分页表的方法,撑死,在内存装满的情况下,顶级页表只需要占用1页而已。
这样,我们可以先把顶级页表全部装入,也就只占1页(一般都装不满),如有需要,再通过顶级页表按需载入二级页表,通过二级页表再引入程序的内容,这样就节省了不少空间
————————————
拓展到64位操作系统
64位,块大小依然是4KB
64位的低12位仍然不动,对应4KB块大小。那高52位如何划分呢?
此时页表项因为不再是32位,一条占4B了,而是64位系统,页表的一条占用了8B
那么页表占用空间是:想要最多只用1页装入,4KB/8B = 2的9次幂,即为9位。因为妄想着最多只用1页索引,那么不可避免的就要反复建立多级页表了
最终就是:64=12+9+9+9+9+9+7
可以看到最终是6个级别的页表才分完
最后的7是没办法的事情,没有32位的极端情况下那么好运刚刚好只占1页
原文链接http://t.csdn.cn/TtA5t

页面置换算法

页面置换也是关于操作系统的知识。大家想详细了解可以看这篇页面置换算法详解(10种) ,关于这个OPT我也是第一次了解,这个只是一个理论上的算法,由于我们无法预知未来,暂时不太能实现,但我寻思靠机器学习和大数据分析是不是这些已经迎刃而解了

最佳置换算法(OPT)(理想置换算法)
最佳置换算法是由 Belady 于1966年提出的一种理论上的算法其所选择的被淘汰页面,将是以后永不使用的, 或许是在最长(未来)时间内不再被访问的页面
采用最佳置换算法,通常可保证获得最低的缺页率。
从 主存 中移出永远不再需要的页面;如无这样的页面存在,则选择最长时间不需要访问的页面。这样可以保证获得最低的缺页率。 即被淘汰页面是以后永不使用或最长时间内不再访问的页面。
例题如下:
物理页面 2 3 2 1 5 2 4 5 3 2 5 2
物理块1 2 2 2 2 4 4
物理块2 3 3 3 3 2
物理块3 1 5 5 5
是否缺页 是 是 是 是 是 是
缺页9次,总访问次数12次
缺页率:6/12 = 50%
另外可以再重点了解一下LRU和FIFO(先进先出)
FIFO算法:最新进入的页面放在表尾,最早进入的页面放在表头。当缺页中断时,淘汰表头的页面并把新调入的页面加到表尾。这种算法的缺点是可能会把有用的页面淘汰掉
最近最少使用页面置换算法(LRU)(Least Recently Used)
在缺页中断发生时,置换未使用时间最长的页面。
LRU理论上是可以实现的,但是代价很高。维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。困难的是在每次访问内存时都必须要更新整个链表
假设用硬件实现:硬件有一个64位计数器C,它在每条指令执行完后自动加1,每个页表项必须有一个足够容纳这个计数器值的域。在每次访问完内存后,将当前的C值保存到被访问页面的页表项中。一旦发生缺页中断,操作系统就检查所有页表项中计数器的值,找到值最小的一个页面,这个页面就是最近最少使用的页面。
但是只有非常少的计算机拥有这样的硬件

带宽

这个是关于计算带宽大小的一道题

总线带宽:指总线在单位时间内可以传输的数据总数(等于总线的宽度与工作频率的乘积)
通常单位:MB/s(MBps)
总线的传输速率=总线的带宽=(总线位宽/8位)*(总线工作频率/总线周期时钟数)
设总线的时钟频率为8MHz,一个总线周期等于一个时钟周期。如果一个总线周期中并行传送16位数据,试问总线的带宽是多少?
解答 :
根据总线时钟频率为8MHz,得 1 个时钟周期为1/8MHz=0.125μs
总线传输周期为0.125μs×1=0.125μs故总线的带宽为 16/(0.125μs)=128MBps
计算总线带宽

Java线程状态

首先我们先明确一下什么是线程,线程是一个比进程更小的执行单位,一个进程在执行过程中可以产生多个线程。与进程不同的是同类的各个线程共享进程的方法区资源,但每个线程有自己的程序计数器,虚拟机栈,本地方法栈,所以系统在产生一个一个线程,或是在各个线程之间切换工作时,负担要比进程小得多,也正因为如此,线程又被称为轻量级进程。
线程的生命周期和状态
Java 线程在运⾏的⽣命周期中的指定时刻只可能处于下⾯ 6 种不同状态的其中⼀个状态(图源《Java 并发编程艺术》4.1.4 节)
在这里插入图片描述
线程在⽣命周期中并不是固定处于某⼀个状态⽽是随着代码的执⾏在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节)在这里插入图片描述由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调⽤ start() ⽅法后开始运⾏,线程这时候处于 READY(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运⾏) 状态

操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到
RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread
States),所以 Java 系统⼀般将这两个状态统称为 RUNNABLE(运⾏中) 状态
在这里插入图片描述当线程执⾏ wait() ⽅法之后,线程进⼊ WAITING(等待) 状态。进⼊等待状态的线程需要依靠其他线程的通知才能够返回到运⾏状态,⽽ TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,⽐如通过 sleep(long millis) ⽅法或 wait(long millis) ⽅法可以将Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞)状态。线程在执⾏ Runnable 的 run() ⽅法之后将会进⼊到 TERMINATED(终⽌) 状态。

String以及intern()方法

谈到String,我们首先想到的就是它是不可变的,可以理解为常量,线程安全,存在常量池中。每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象

String为什么是不可变的?

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}

需要注意的是根本原因不是final关键字修饰字符数组来保存字符串而使之不可变
我们知道被final修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。要注意这数保存的字符串是可变的(final修饰引用类型变量的情况)
String真正不可变有以下几点原因

  1. 保存字符串的数组被final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法
  2. String类被final修饰导致其不能被继承,进而避免了自类破坏String不可变

相关阅读:如何理解 String 类型值的不可变? - 知乎提问
补充(来自issue 675):在 Java 9 之后,StringStringBuilderStringBuffer 的实现改用 byte 数组存储字符串。

Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?
新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,bytechar 所占用的空间是一样的。
这是官方的介绍:https://openjdk.java.net/jeps/254

public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
 // @Stable 注解表示变量最多被修改一次,称为“稳定的”。
 @Stable
 private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
 byte[] value;

}

字符串拼接用“+” 还是 StringBuilder?

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象

不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了

String s1 = new String(“abc”);这句话创建了几个字符串对象?

会创建 1 或 2 个字符串:

  • 如果字符串常量池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”
  • 如果字符串常量池中没有字符串常量“abc”,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象

验证

String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出 true

String 类型的变量和常量做“+”运算时发生了什么?

一个非常常见的面试题。

先来看字符串不加 final 关键字拼接的情况(JDK1.8):

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

注意 :比较 String 字符串的值是否相等,可以使用 equals() 方法。 String 中的 equals 方法是被重写过的。 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是字符串的值是否相等。如果你使用 == 比较两个字符串是否相等的话,IDEA 还是提示你使用 equals() 方法替换。

对于基本数据类型来说,== 比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa==bb);// true

JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。

并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string";

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量。
  • final 修饰的基本数据类型和字符串变量
  • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

因此,str1str2str3 都属于字符串常量池中的对象。

引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

String str4 = new StringBuilder().append(str1).append(str2).toString();

因此,str4 并不是字符串常量池中存在的对象,属于堆上的新对象。

我画了一个图帮助理解:在这里插入图片描述
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer

不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true

final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。

如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。

示例代码如下(str2 在运行时才能确定其值):

final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
      return "ing";
}

我们再来看一个类似的问题!

String str1 = "abcd";
String str2 = new String("abcd");
String str3 = new String("abcd");
System.out.println(str1==str2);
System.out.println(str2==str3);

上面的代码运行之后会输出什么呢?

答案是:

false
false

这是为什么呢?

我们先来看下面这种创建字符串对象的方式:

// 从字符串常量池中拿对象
String str1 = "abcd";

这种情况下,jvm 会先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd";

因此,str1 指向的是字符串常量池的对象。

我们再来看下面这种创建字符串对象的方式:

// 直接在堆内存空间创建一个新的对象。
String str2 = new String("abcd");
String str3 = new String("abcd");

只要使用 new 的方式创建对象,便需要创建新的对象

使用 new 的方式创建对象的方式如下,可以简单概括为 3 步:

  1. 在堆中创建一个字符串对象
  2. 检查字符串常量池中是否有和 new 的字符串值相等的字符串常量
  3. 如果没有的话需要在字符串常量池中也创建一个值相等的字符串常量,如果有的话,就直接返回堆中的字符串实例对象地址。

因此,str2str3 都是在堆中新创建的对象。

字符串常量池比较特殊,它的主要使用方法有两种:

  1. 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
  2. 如果不是用双引号声明的 String 对象,使用 String 提供的 intern() 方法也有同样的效果。String.intern() 是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后,字符串常量池被从方法区拿到了堆中,jvm 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销

示例代码如下(JDK 1.8) :

String s1 = "Javatpoint";
String s2 = s1.intern();
String s3 = new String("Javatpoint");
String s4 = s3.intern();
System.out.println(s1==s2); // True
System.out.println(s1==s3); // False
System.out.println(s1==s4); // True
System.out.println(s2==s3); // False
System.out.println(s2==s4); // True
System.out.println(s3==s4); // False

总结

  1. 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
  2. 在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
  3. 一般来说,我们要尽量避免通过 new 的方式创建字符串。使用双引号声明的 String 对象( String s1 = "java" )更利于让编译器有机会优化我们的代码,同时也更易于阅读。
  4. final 关键字修改之后的 String 会被编译器当做常量来处理,编译器程序编译期就可以确定它的值,其效果就相当于访问常量。

参考

BlockingQueue及其实现

BlockingQueue即阻塞队列,它是基于ReentrantLock,依据它的基本原理,我们可以实现Web中的长连接聊天功能,当然其最常用的还是用于实现生产者与消费者模式,大致如下图所示:在这里插入图片描述

在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的

想了解详情,参考BlockingQueue及其实现

结语

暂时就只能记得这些知识点,后续如果想起会补充,互联网寒冬归寒冬,我们多学一点就行了,沉下心来慢慢去学习这些知识相信总会有匹配的岗位来到我们身边。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值