Java面试(基础篇)——解构Java常见的基础面试题 & 结合Java源码分析_java源码面试题

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新软件测试全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注软件测试)
img

正文

链式寻址法+红黑树解决hash 冲突

HashMap 在JDK1.8 版本中,通过链式寻址法+红黑树的方式来解决hash 冲突问题,其中红黑树是为了优化Hash 表链表过长导致时间复杂度增加的问题。当链表长度大于8 并且hash 表的容量大于64 的时候,再向链表中添加元素就会触发转化。

在这里插入图片描述

当链表长度大于8 并且hash 表的容量大于64 的时候,再向链表中添加元素就会触发转化。

在这里插入图片描述

HashMap 中的hash 方法为什么要右移16 位异或

在这里插入图片描述

之所以要对hashCode 无符号右移16 位并且异或,核心目的是为了让hash 值的散列度更高,尽可能减少hash 表的hash 冲突,从而提升数据查找的性能。

首先使用key 的hashCode 无符号右移16 位,意味着把hashCode 的高位移动到了低位。

然后再用hashCode 与右移之后的值进行异或运算,就相当于把高位和低位的特征进行和组合。

从而降低了hash 冲突的概率。

受检异常和非受检异常

Java基础(8)——java的异常机制初步 & 异常的捕获和处理 & 自定义异常

受检异常和非受检异常,都是继承自Throwable 这个类中,分别是Error 和Exception,

  • Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。
  • Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被jvm 处理。

在这里插入图片描述

  1. 受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法
    (1)通过try/catch 捕获该异常;
    (2)通过throw 把异常抛出去;
  2. 非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如
    NullPointException

在这里插入图片描述

受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法

在这里插入图片描述

为什么阿里巴巴的Java 开发手册不建议使用Java 自带的线程池

了解Java中线程池

Java进阶(5)——创建多线程的方法extends Thread和implements Runnable的对比 & 线程池及常用的线程池

4.【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

在这里插入图片描述

Executors 里面默认提供的几个线程池是有一些弊端的,如果是不懂多线程、或者是新手直接盲目使用,就可能会造成比较严重的生产事故。

为什么不能用

  • 1.FixedThreadPool 和SingleThreadPool 中,阻塞队列长度是Integer.Max_Value,一旦请求量增加,就会堆积大量请求阻塞在队列中,可能会造成内存溢出的问题;

在这里插入图片描述

  • 2.CachedThreadPool 和ScheduledThreadPool 中最大线程数量是Integer.Max_value,一旦请求量增加,导致创建大量的线程,使得处理性能下降。

在这里插入图片描述

JDK 动态代理为什么只能代理有接口的类

在Java 里面,动态代理是通过Proxy.newProxyInstance()方法来实现的,它需要传入被动态代理的接口类。

在这里插入图片描述
加入如下代码进行运行:

System.getProperties().put(“sun.misc.ProxyGenerator.saveGeneratedFiles”, “true”);

或者加入下面这句

System.getProperties().put(“jdk.proxy.ProxyGenerator.saveGeneratedFiles”, “true”);

在这里插入图片描述
JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类会继承java.lang.reflect.Proxy 类,同时还会实现被代理类的接口。

在Java 中,是不支持多重继承的,而每个动态代理类都会继承Proxy 类(这也是JDK动态代理的实现规范) ,所以就导致JDK 里面的动态代理只能代理接口,而不能代理实现类。

在这里插入图片描述

spring中的代理

Spring进阶(AOP的理解)——静态/动态代理 & 面向切面编程AOP(Aspect Oriented Programming) & 日志记录 & 增强方法

在这里插入图片描述

如果一定要针对普通类来做动态代理,可以选择cglib 这个组件,它会动态生成一个被代理类的子类,子类重写了父类中所有非final 修饰的方法,在子类中拦截父类的所有方法调用从而实现动态代理。

Java对象相关的面试题

对象的创建过程

在这里插入图片描述

(1)类加载检查
JVM 首先会去检查目标对象是否已经被加载并初始化了。

如果没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面。

然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。

(2)分配内存空间

当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对象的大小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。

内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据Java 堆内存是否规整来决定内存分配方式。

(3)初始化 ”零值”

JVM 会把目标对象里面的普通成员变量初始化为零值,比如int 类型初始化为0 ,对象类型初始化为null, (类变量在类加载的准备阶段就已经初始化过了) 。

这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序能够获得这些字段对应数据类型的零值。

(3)设置对象头”

然后,JVM 还需要对目标对象的对象头做一些设置,比如对象所属的类元信息、对象的GC 分代年龄、hashcode、锁标记等等。

(4)执行init方法

完成这些步骤以后,对于JVM 来说,新对象的创建工作已经完成。但是对于Java 语言来说,对象创建才算是开始。

接下来要做的,就是执行目标对象内部生成的init 方法,初始化成员变量的值、执行构造块、最后执行目标对象的构造方法,完成对象的创建。

其中,init 方法是Java 文件编译之后在字节码文件中生成的,它是一个实例构造器,这个构造器会把语句块、变量初始化、调用父类构造器等操作组织在一起。

所以调用init方法能够完成一系列的初始化动作。

什么是深拷贝和浅拷贝?

Java进阶(4)——结合类加载JVM的过程理解创建对象的几种方式:new,反射Class,克隆clone(拷贝),序列化反序列化

浅拷贝

在这里插入图片描述

深拷贝

在这里插入图片描述

深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。

浅拷贝, 就是只复制某个对象的指针,而不复制对象本身。这种复制方式意味着两个引用指针指向被复制对象的同一块内存地址。

在Java 里面,无论是深拷贝还是浅拷贝,都需要通过实现Cloneable 接口,并实现clone()方法。然后我们可以在clone()方法里面实现浅拷贝或者深拷贝的逻辑。

实现深拷贝的方法有很多,比如

  1. 通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反序列化回来,就会得到一个完整的新对象。
  2. 在clone()方法里面重写克隆逻辑,也就是对克隆对象内部的引用变量再进行一次克隆。

Book.java实体类

implements Cloneable{ // 可以克隆的

package com.tianju.auth.reflect;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book implements Cloneable{ // 可以克隆的
private String title;
private Author author;
public double price;

static {
System.out.println(“book的静态代码块”);
}

// protected:代表本包或者继承
// 继承的时候,可以将子类的访问控制符扩大,但不能缩小;
// 子类不能比父类抛出更多的异常
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}

public Book deepClone(){
Book book = new Book();
Author au = new Author();
au.setName(author.getName());
book.setAuthor(au);
book.setTitle(this.title);
book.setPrice(this.price);
return book;
}
}

进行测试

在这里插入图片描述

package com.tianju.auth.reflect;

public class TestDemo{
public static void main(String[] args) throws CloneNotSupportedException {
Author author = new Author();
author.setName(“吴承恩”);
Book book = new Book(“三国演义”, author,12.56);
Book book1 = book;

System.out.println(book1==book);// == 两个引用是否指向同一个对象

// clone创建了一个新的对象,只是值一样
Book bookClone = (Book) book.clone();
// 深拷贝,创建了新的对象,上面的浅拷贝,只是拷贝了引用
Book deepClone = book.deepClone();

System.out.println(bookClone==book);
System.out.println(“克隆前:”+book);
System.out.println(“克隆后:”+bookClone);

author.setName(“小柯基”);
System.out.println(“修改后的原对象:”+book);
System.out.println(“修改后的clone对象:”+bookClone);

// 深拷贝
System.out.println(“***********”);
System.out.println(“深拷贝的方法:”+deepClone);
}
}

String相关的面试题

new String(“abc”)到底创建了几个对象

  1. 如果abc 这个字符串常量不存在,则创建两个对象,分别是abc 这个字符串常量,以及new String 这个实例对象。
  2. 如果abc 这字符串常量存在,则只会创建一个对象

Java基础(1)——数据类型&包装类,引用类型String&StringBuilder,正则表达式,定点数,日期类

(1)String str = new String(“hello”);

执行上述代码,底层进行了如下工作,在栈内存里存放变量str,在堆内存新创建一个String的对象,在常量池空间中创建常量hello,如果存在则不创建;创建了一个或两个对象。
在这里插入图片描述

(2)String str = “hello”;

栈内存中的str指向常量池中的hello
在这里插入图片描述

String、StringBuffer、StringBuilder 区别

(1)可变性
String 内部的value 值是final 修饰的,所以它是不可变类。所以每次修改String 的值,都会产生一个新的对象。

StringBuffer 和StringBuilder 是可变类,字符串的变更不会产生新的对象。

在这里插入图片描述

StringBuffer 和StringBuilder 是可变类

在这里插入图片描述

(2)线程安全性
String 是不可变类,所以它是线程安全的。

StringBuffer 是线程安全的,因为它每个操作方法都加了synchronized 同步关键字。
StringBuilder 不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用StringBuffer ,否则使用StringBuilder

在这里插入图片描述

(3)性能方面
String 的性能是最的低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新
创建新的对象以及分配内存。

其次是StringBuffer 要比String 性能高,因为它的可变性使得字符串可以直接被修改,最后是StringBuilder ,它比StringBuffer 的性能高,因为StringBuffer 加了同步锁。

在这里插入图片描述

(4)存储方面
String 存储在字符串常量池里面;
StringBuffer 和StringBuilder 存储在堆内存空间。

最后再补充一下, StringBuilder 和StringBuffer 都是派生自AbstractStringBuilder这个抽象类。

Integer相关的面试题

Integer 和int 的区别?Java 为什么要设计封装类?

Integer 是基本数据类型int 的封装类,在Java 里面,有八种基本数据类型,他们都有一一对应的封装类型。
基本类型和封装类型的区别有很多,比如

  • int 类型,我们可以直接定义一个变量名赋值即可,但是Integer 需要使用new 关键字创建对象;
  • 基本类型和Integer 类型混合使用时,Java 会自动通过拆箱和装箱实现类型转换
  • Integer 作为一个对象类型,封装了一些方法和属性,我们可以利用这些方法来操作数据。
  • 作为成员变量, Integer 的默认值是null ,而int 的默认值是0

在Java 里面,之所以要对基础类型设计一个对应的封装类型。是因为Java 本身是一门面向对象的语言,对象是Java 语言的基础单元,我们时时刻刻都在创建对象,也随时都在使用对象,很多时候在传递数据时也需要对象类型,比如像ArrayList、HashMap 这些集合,只能存储对象类型,因此从这个点来说,封装类型存在的意义就很大。

其次,封装类型还有很多好处,比如

  • 安全性较好,可以避免外部操作随意修改成员变量的值,保证了成员变量和数据传递的安全性
  • 隐藏了实现细节,对使用者更加友好,只需要调用对象提供的方法就可以完成对应的操作

Integer 和int 的区别有很多,总结以下3点:
(1)Integer 的初始值是null ,int 的初始值是0
(2)Integer 存储在堆内存,int 类型是直接存储在栈空间
(3)Integer 是对象类型,它封装了很多的方法和属性,我们在使用的时候更加灵活。

至于为什么要设计封装类型,最主要的原因是Java 本身是面向对象的语言,一切操作都是以对象作为基础。比如像集合里面存储的元素,也只支持存储Object 类型,普通类型无法通过集合来存储。

Integer 使用不当导致生产的事故

为什么两个Integer 的对象不能用==号来判断?

在这里插入图片描述

Integer 是一个封装类型。它是对应一个int 类型的包装。

在Java 里面之所以要提供Integer 这种基本类型的封装类,是因为Java 是一个面向对象的语言,而基本类型不具备对象的特征,所以在基本类型上做了一层对象的包装并且提供了相关的属性和访问方法来完善基本类型的操作。

在Integer 这个封装类里面,除了基本的int 类型的操作之外,还引入了享元模式的设计,对-128 到127 之间的数据做了一层缓存,也就是说,如果Integer 类型的目标值在-128 到127 之间,就直接从缓存里面获取Integer 这个对象实例并返回,否则创建一个新的Integer 对象。

在这里插入图片描述

这么设计的好处是减少频繁创建Integer 对象带来的内存消耗从而提升性能。

因此在这样一个前提下,如果定义两个Integer 对象,并且这两个Integer 的取值范围正好在-128 到127 之间。

如果直接用==号来判断,返回的结果必然是true ,因为这两个Integer 指向的内存地址是同一个。否则,返回的结果是false。

Integer a1 =100 Integer a2 =100 ,a1 ==a2?的运行结果?

按照大家对于Java 基础的认知,两个独立的对象用==进行比较,是比较两个对象的内存地址。那得到的结果必然是false。但是在这个场景中,得到的结果是true。

为什么呢?

首先, Integer a1 =100, 把一个int 数字赋值给一个封装类型,Java 会默认进行装箱操作,也就是调用Integer.valueOf()方法,把数字100 包装成封装类型Integer。

在这里插入图片描述

其次,在Integer 内部设计中,用到了享元模式的设计,享元模式的核心思想是通过复用对象,减少对象的创建数量,从而减少内存占用和提升性能。

在这里插入图片描述

Integer 内部维护了一个IntegerCache,它缓存了-128 到127 这个区间的数值对应的Integer 类型。

一旦程序调用valueOf 方法,如果数字是在-128 到127 之间就直接在cache 缓存数组中去取Integer 对象;否则,就会创建一个新的对象。

所以,对于这个面试题来说,两个Integer 对象,因为值都是100,并且默认通过装箱机制调用了valueOf 方法。从IntegerCache 中拿到了两个完全相同的Integer 实例。因此用等号比较得到的结果必然是true。

总结,a1 ==a2 的执行结果是true。原因是Integer 内部用到了享元模式的设计,针对-128 到127 之间的数字做了缓存。使用Integer a1 =100 这个方式赋值时,Java 默认会通过valueOf 对100 这个数字进行装箱操作,从而触发了缓存机制,使得a1 和a2 指向了同一个Integer 地址空间。

**Tips:**在工作中直接把两个Integer 封装类型用等号去比较,就有可能导致生产故障。

ArrayList 的自动扩容机制

Java进阶(3)——手动实现ArrayList & 源码的初步理解分析 & 数组插入数据和删除数据的问题

在这里插入图片描述

ArrayList 是一个数组结构的存储容器,默认情况下,数组的长度是10.
当然我们也可以在构建ArrayList 对象的时候自己指定初始长度。

随着在程序里面不断的往ArrayList 中添加数据,当添加的数据达到10 个的时候,ArrayList 就没有多余容量可以存储后续的数据。

这个时候ArrayList 会自动触发扩容。

扩容的具体流程很简单,

  1. 首先,创建一个新的数组,这个新数组的长度是原来数组长度的1.5 倍。
  2. 然后使用Arrays.copyOf 方法把老数组里面的数据拷贝到新的数组里面。扩容完成后再把当前要添加的元素加入到新的数组里面,从而完成动态扩容的过程。

强引用、软引用、弱引用、虚引用有什么区别

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

(1)强引用
就是普通对象的引用,只要还有强引用指向一个对象,就能表示对象还“活着”,垃圾收集器无法回收这一类对象。

只有在没有其他引用关系,或者超过了引用的作用域,再或者显示的把引用赋值为null的时候,垃圾回收器才能进行内存回收。

(2)软引用
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象。

软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

(3)弱引用
相对强引用而言,它允许在存在引用关联的情况下被垃圾回收的对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,

一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,垃圾回收期都会回收该内存

(4)虚引用
它不会决定对象的生命周期,它提供了一种确保对象被finalize 以后,去做某些事情的机制。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
圾收集,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象。

软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

(3)弱引用
相对强引用而言,它允许在存在引用关联的情况下被垃圾回收的对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,

一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,垃圾回收期都会回收该内存

(4)虚引用
它不会决定对象的生命周期,它提供了一种确保对象被finalize 以后,去做某些事情的机制。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
[外链图片转存中…(img-QZ22r4JZ-1713137179973)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值