创作于9月23日提前面试xx集团中的感受(第一次面试)
首先自然是对技术官做一个个人介绍,这里要分两种面试场景,所以务必准备好两种自我介绍模板!!
1、对技术面试官:
- 其中主要讲解自己大学在编程方面干了什么,通过学习掌握了那些技术栈,做了什么项目,在那个技术栈比较有深入的理解。突出自己的亮点,核心竞争力在哪里
2、对HR来讲
- 主要讲一讲自己在大学的丰富校园生活,有没用学生管理经验(学生会,社联等等),技术栈笼统带过就好。
发现了一些问题
- 对于应届生而言,最重要的不是框架,而是Java的基础,应届生面试,面试官特别喜欢在一面问一些基础。
- 所以一定要把Java基础打牢。其次面试官很少会问你框架怎么用之类的问题,只会问为什么用到这个技术,这个方案有什么优点,让你聊一聊
- 还有一些技术性框架比较有标志性的问题会常问,比如说:Spring的IOC和AOP,SpringBoot启动类注解干了什么等等
- 注意主次,一定要畅谈自己熟悉的领域,对自己不熟悉的一笔带过。
接下来就是技术面试官发问环节(分知识点整理如下):
1、JVM
1.1、双亲(parent)委派机制是什么?谈谈你对他的理解?
原文链接:https://blog.csdn.net/Dream_ling/article/details/109450588
- 双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
具体流程是这样的:
1.当Application(应用) ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
2.当Extension(扩展) ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
3.如果Bootstrap(引导/启动) ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
4.如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
5.如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
6.如果均加载失败,就会抛出ClassNotFoundException异常。
打个比方:
当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到到达Bootstrap classLoader之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
1.2、既然你提到了类加载器,谈谈你对类加载器知道多少?
4:除此之外还有第四种类加载器:自定义类加载器,通过继承java.lang.ClassLoader类的方式实现自己的类加载器。
这又不得不提到JVM的执行Java程序的过程:我们写的Java程序(.java)先会被编程成二进制字节码文件(.class),然后通过jvm中的类加载器加载到Java虚拟机中,然后才会被Java虚拟机解析。
2、Mysql
MySQL优化有哪些常见方法?
(这一块需要恶补!)
这是一些sql语句的优化方法:
- 查询语句中不要使用select *
- 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
- 减少使用IN或者NOT IN ,使用exists,not exists或者关联查询语句替代
- or 的查询尽量用 union或者union all 代替(在确认没有重复数据或者不用剔除重复数据时, union all会更好)
- 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。(这一块不是很了解)
- 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表 扫描,如:
select id from t where num is null 可以在num上设置默认值0,确保表中num列没有 null值,然后这样查询: select id from t where num=0
3、SpringBoot
原文链接:SpringBoot启动类及其原理
SpringBoot启动类在启动时会干些什么事情?你对启动类的注解知道多少?
SpringBoot的启动类上使用@SpringBootApplication注解标识,该注解试一个组合注解,包含多个其他注解。
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan是Spring中的注解,用来与配置类上,定义要扫描的组件。 上面两个注解则是SpringBoot自定义的注解。
- @SpringBootConfiguration:@SpringBootConfiguration注解中没有定义任何属性信息,而该注解上有一个注解@Configuration,用于标识配置类。所以@SpringBootConfiguration注解的功能和@Configuration注解的功能相同,用于标识配置类
- @EnableAutoConfiguration:这个注解就是SpringBoot最强大的一个功能(自动装配)!
这个注解也是一个组合注解,除了元注解外主要是由下面两个注解组成:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
-
@AutoConfigurationPackage中使用注解@Import(@Import:的作用)导入了AutoConfigurationPackages.Registrar.class到容器中,那么来分析这个类,进入到这个内部类Regisrar:
如果觉得不耐烦,请看原文链接
!@AutoConfigurationPackage
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); } @Override public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new PackageImports(metadata)); } }
该类引入的重点在于方法registerBeanDefinitions():
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); }
首先先分析方法体中所调用的方法register()的第二个参数
PackageImports(metadata).getPackageNames().toArray(new String[0])
进入到类PackageImports的构造方法:
PackageImports(AnnotationMetadata metadata) { AnnotationAttributes attributes = AnnotationAttributes .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false)); List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages"))); for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) { packageNames.add(basePackageClass.getPackage().getName()); } if (packageNames.isEmpty()) { packageNames.add(ClassUtils.getPackageName(metadata.getClassName())); } this.packageNames = Collections.unmodifiableList(packageNames); }
在这个构造方法中将元数据即启动类AnnotationMetadata metadata经过处理获取标签注解信息,注解信息里面的 basePackages 和 basePackageClasses是否有数据。basePackages、 basePackageClasses为注解@AutoConfigurationPackage中的属性。如果没有数据则获取注解所在的类的名字目录,放到List中
获得packageNames属性也就是启动类所在的包。回到Registrar中的registerBeanDefinitions()方法中register()方法的第二个参数即为启动类所在的包的名称,并且使用数组来进行表示。
在分析**register()**方法,register()源码如下:
public static void register(BeanDefinitionRegistry registry, String... packageNames) { if (registry.containsBeanDefinition(BEAN)) { BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN); beanDefinition.addBasePackages(packageNames); } else { registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames)); } }
这个方法的if语句为判断registry这个参数中是否已经注册了AutoConfigurationPackages的类路径所对应的bean(AutoConfigurationPackages)。如若已经被注册,则把上面分析的第二个参数所获取的包(启动类所在的包的名称)添加到这个bean的定义中。如若没有,则注册这个bean并且把包名设置到该bean的定义中。
小结:@AutoConfigurationPackage就是添加该注解的类所在的包作为自动配置包进行管理。他的实现就是依赖于工具类AutoConfigurationPackages中的内部类Registrar对所标注的包进行注册。
- @Import(AutoConfigurationImportSelector.class)自动配置导入选择器
小结:@EnableAutoConfiguration的实现方式是导入配置文件META-INF/spring.factories中EnableAutoConfiguration所对应的所有的类。
顺带谈谈SpringBoot自动装配的实现:
自动装配的实现主要依靠三个核心关键技术。 引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对 象。 这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。 这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是 用到了 Spring 里面的 SpringFactoriesLoader 来完成的。 SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的 ImportSelector 接口(类似于Spring中的import注解),实现对这些配置类的动态加载。
4、Java基础
4.1、== 和 equals 的区别:
首先从概念上入手:
==:
- 基本类型:比较的是基本类型具体的值是否相等
- 引用类型(eg:String):比较的是引用类型的地址是否相等,因为引用类型的值的就是对象的地址
equals:
- equals是Java所有类的祖先Object类中的方法,这个方法在Object中的作用和
==
其实是一样的,比较的是地址是否相等,但是话说回来了,咱们用到的Java所有的包装类比如说String都是重新了Object类的equals()方法的,所以在包装类中,equals方法比较的是值是否相等而不是地址是否相等!
看个代码思考思考:
String str1 = new String("123456");
String str2 = new String("123456");
System.out.println(str1==str2);
//因为new出来的对象都会存放在虚拟机的堆内存区域,所以每次new都会分配地址,==对于引用类型来说比较的是地址,所以为false
//因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
System.out.println(str1.equals(str2));//equals在包装类中比较的是值
相信学习过jvm的都知道有个东西叫Stringtable(串池)吧,串池存在的意义就是避免字符串重复。
提到了stringtable就不得不说一说运行时常量池和方法区了:
4.1.1、方法区
4.1.1.1、定义:
Java 虚拟机具有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于常规语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法 (§2.9)。
方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾回收或压缩它。此规范不规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,也可以根据计算的要求进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。
Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在大小不同的方法区域的情况下,控制最大和最小方法区域的大小。
以下异常情况与方法区域相关联:
- 如果方法区域中的内存无法用于满足分配请求,则 Java 虚拟机将引发 .
OutOfMemoryError
4.1.1.2、组成:
- 1.8以前 永久代(堆内存)
- 1.8之后 元空间(本地内存)
4.1.1.3、常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
也叫 class文件常量池,主要存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)。
- 字面量:例如文本字符串、fina修饰的常量。
int i = 2;
String str = "abcdefg";
- 符号引用:例如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
4.1.1.4、运行时常量池
*运行时常量池,常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
4.1.1.5、StringTable(串池)
字符串常量池,也可以理解成运行时常量池分出来的一部分。类加载到内存的时候,字符串会存到字符串常量池里面。利用池的概念,避免大量频繁创建字符串。
从上面两个图,我们可以知道
- 在jdk1.8中,串池存储在常量池中
- 在jdk1.7之后,串池存储在堆内存中
实际上在java中,存在很多这样的常量池。其目的只有一个,就是为了复用,节约内存。
这里先给出两个概念:
- 字符串常量拼接(编译期间优化)
- 字符串变量拼接
看一下这段代码可能你就懂了:
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的,为什么是懒惰的,因为他在未javac之前只是个字面量还不是字符串对象,只要编译后才能确定他是一个字符串对象
String s2 = "b";
String s3 = "ab";
//String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
//String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
//System.out.println(s3 == s5);
}
}
接下来咱们一起看几道笔试题,顺便分析分析:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
System.out.println(s3 == s6); //true
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);//false
从头开始分析:
记住一句话,如果是new 出来的对象一定在堆中,如果是字面量,才会加入到常量池中
代码序号 | 串池元素(StringTable) | 当前字符串所在位置 | 当前序号代码分析 |
---|---|---|---|
1 | “a” | s1在串池 | 尝试将"a"这个字符串常量加入到串池中,由于串池中没由"a"这个字符串常量,所以加入串池成功! |
2 | “a”,“b” | s2在串池 | 尝试将"b"这个字符串常量加入到串池中,由于串池中没由"b"这个字符串常量,所以加入串池成功! |
3 | “a”,“b”,“ab” | s3在串池 | 字符串常量在编译期间优化,相当于s3=“ab”,尝试将"ab"这个字符串常量加入到串池中,由于串池中没由"ab"这个字符串常量,所以加入串池成功! |
4 | “a”,“b”,“ab” | s4在堆中 | 字符串变量拼接,本质:new StringBuilder().append(s1).append(s2).toString() 相当于new String(“ab”) |
5 | “a”,“b”,“ab” | s5指向串池中的"ab" | 由于s5=“ab”,在常量池中以及有了"ab",所以加入串池失败了,s5就直接指向串池中的"ab"字符串 |
6 | “a”,“b”,“ab” | s6指向串池中的"ab" | s4.intern()的意思是尝试把s4的值加入到串池中,但是串池中以及有了,就加入失败,但是会返回串池中"ab"的位置,结果就是s6指向串池中的"ab" |
12 | “a”,“b”,“ab” | x2在堆中 | 这行代码的意思就是x2 = new String(“cd”) ,所以x2一定是在堆中的 |
13 | “a”,“b”,“ab”,“cd” | x1在串池中 | 尝试将"cd"这个字符串常量加入到串池中,由于串池中没由"cd"这个字符串常量,所以加入串池成功! |
14 | “a”,“b”,“ab”,“cd” | x2还是在堆中 | 尝试将x2加入串池,但是堆中以及有了"cd",加入串池失败,x2仍然在堆中。特别说明:intern()方法会返回当前常量池的值 |
第13和14行代码调换位置后,16行的结果又是什么呢?
代码逻辑就是x2入池,x2在串池中,x1=“cd”,x1引用了串池中的"cd",也就是x1指向了"cd",x1==x2,结果为true
在jdk1.6中,最后一行的结果又会不一样,是因为:
jdk1.6中,intern的用法和1.8有区别:1.6中,x2.intern()的过程是:先将x2复制一份副本,然后将副本入池,但是副本和x2本体不是一个概念,他们所处位置不同,副本在串池中,x2本身是在堆中的。x1指向了x2的副本,所以x1 == x2的结果是false
4.2、final、finally 和 finalize都是什么?
- final:final 修饰类、属性和方法
- 修饰类:final 修饰的类不允许其他类继承,也就是说,final 修饰的类是独一无二的
- 修饰方法:final 修饰的方法不允许被重写
- 修饰属性:final 修饰的变量不能被修改
- 修饰基本类型: final 修饰的变量不能被修改
- 修饰引用类型:final 修饰的引用类型,只是保证对象的引用不会改变。对象内部的数据可以改变。
- finally:finally通常配合try/catch代码块使用,由于finally中的代码最终都会被执行,所以finally中代码常见的都是解锁,流关闭以及连接 释放等操作!
- finalize:finalize()方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。
finalize详解:
- 由于垃圾对象的回收时机具有不确定性,所以finalize也具有不确定性。
- finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存。
- finalize 现在已经不再推荐使用,在 JDK 1.9 中已经明确的被标记为
deprecated
(官方提出的不建议使用,但是可以使用,只是不建议) - 由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达(对象可达性分析是垃圾回收是判定是否回收的依据)开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。
- 另外,重写finalize()方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。
- 利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。(通俗来说就是让对象再活一次)
4.3、如何解决hash碰撞(hash冲突)?
原文链接:(14条消息) 通俗解释hash碰撞是什么以及如何解决_启四的博客-CSDN博客_什么是hash碰撞
首先我们要知道什么是hash?什么是哈希碰撞?
什么是hash表?
hash表的本质就是个数组,只不过这个数组中的元素是个Entry(键值对)
这里的学号是个key,哈希表就是根据key值来通过哈希函数计算得到一个值,这个值就是数组的下标值,用来确定这个Entry要存放在哈希表中哪个位置。
什么是哈希碰撞(冲突)?
hash碰撞指的是,两个不同的值(比如小陈、小吴的学号)经过hash计算后,得到的hash值相同,后来的小吴要放到原来的小陈的位置,但是数组的位置已经被小陈占了,导致冲突。
怎么解决hash碰撞(冲突)?
原文链接:(14条消息) 解决哈希冲突(四种方法)_君诀的博客-CSDN博客_解决哈希冲突
解决hash冲突的思想基本就是:让哈希表的元素存放位置唯一。
大致有几种方法:
- 开放定址法(线性探测法):就是从发生冲突的那个位置开始,按照一定的 次序从 hash 表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲 位置中。ThreadLocal 就用到了线性探测法来解决 hash 冲突的。平方探测法(二次探测)也是相同的道理,只不过找空闲位置的规则不同。
- 链式寻址法:这是一种非常常见的方法,简单理解就是把存在 hash 冲突的 key, 以单向链表的方式来存储,比如 HashMap 就是采用链式寻址法来实现的。 向这样一种情况,存在冲突的 key 直接以单向链表的方式进行存储
- 再hash法:就是在冲突的hash(key)位置在进行一次哈希操作,可以自定义多个哈希方法,这个冲突了,就换一个哈希方法继续哈希,总能找到空闲的位置
- 建立公共溢出区:就是把 hash 表分为基本表和溢出表两个部分,凡事存在冲突 的元素,一律放入到溢出表中。
5、多线程
这块需要系统学习
线程创建的方式有哪几种?
- 继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过实现Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
6、Spring
原文链接:(14条消息) 聊聊Spring的两大核心技术之一AOP_我是大肥鼠的博客-CSDN博客
聊一聊Spring中的AOP?
先可以说说OOP(面向对象编程):继承,封装,多态
OOP允许我们定义从上到下的关系,但并不适合定义从左到右的关系。所以当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。比如最常用的日志功能。这时候就要引用我们的AOP了,面向切面编程。他可以对模块做横切,横向织入代码。他的底层实现思想是动态代理,比如说中介,婚介公司。
AOP可以在不改变原有代码逻辑的基础上,进行代码的功能增强,比如说AOP实现日志输出。
其中有几个概念:
- 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。
- 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。(增强的具体方法,增强的内容)
- 目标(Target):被通知对象。
- 代理(Proxy):向目标对象应用通知之后创建的对象。(如:房屋中介)
- 切入点(PointCut):切面通知 执行的 “地点”的定义。(可以在业务方法上)
- 连接点(JointPoint):与切入点匹配的执行点。
SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:
- 前置通知 :方法执行前调用 ,对应的注解是 @Before
- 后置通知 :方法执行后调用 ,对应的注解是**@After**
- 返回通知 :方法返回后调用 ,对应注解是**@AfterReturning**
- 异常通知 :方法出现异常调用,对应注解是**@AfterThrowing**
- 环绕通知 :动态代理、手动推荐方法运行,对应的注解是**@Around**
AOP和OOP都是为了降低代码的耦合性,提高复用性,用来提高代码开发效率。
AOP的动态代理方式有两种:
- cglib
- 默认是jdk
具体的区别看这篇:
(14条消息) AOP中的动态代理的区别–JDK和CGLIB_黄泥川水猴子的博客-CSDN博客_aop cglib和jdk动态代理