Java基础要求
企业在java基础层面试要求主要包括如下几个点:
扎实JAVA基本功、熟练应用集合、I/O、多线程、网络编程、反射API以及lambda、stream等技术。
CGLIB
熟悉JVM体系结构、类加载、运行时内存区、字节码增强技术、GC常用算法以及
JVM调优策略。
Java编程基础
案例代码
-
变量赋值
哪几种数据类型 byte char short int long float double boolean
java中char不等于字符
Char与字符概念不同
java中char类型目前已经偏离了其原有的语意,也就是一个char类型表示的并不完全是我们理解上的任意一个字符,这是由于java中char类型所使用的utf-16编码的历史原因造成的。
有一点理解的没有错,java中char类型就是使用固定两个字节来表示。
jvm规范中是如下描述的:
char
, whose values are 16-bit unsigned integers representing Unicode code points in the Basic Multilingual Plane, encoded with UTF-16, and whose default value is the null code point ('\u0000'
)。
也就是java中char类型使用16位无符号整数来描述unicode中最早一批收录的字符,这批字符后来被叫作Basic Multilingual Plane(BMP),然后编码表使用的是utf-16。
所以,java中单个char类型描述的字符是有限的,单个char只能描述unicode中的BMP范围的码位,也就意味着BMP范围外的字符char是无法表示的。
utf-16如何表示non-BMP中的字符
utf-16使用4个字节来表示non-BMP中的字符,这4个字节以两个字节为单位划分,也就是可以分为两个16-bit的code unit(码元)
Java语言规范规定,Java的char类型是UTF-16的code unit,也就是一定是16位(2字节);
char, whose values are 16-bit unsigned integers representing UTF-16 code units(码元) (§3.1).
然后字符串是UTF-16 code unit的序列:
The Java programming language represents text in sequences of 16-bit code units, using the UTF-16 encoding.
char类型的字面值演示:
public class DataTypeTests {
public static void main(String[] args) {
char c1;
c1='B';
c1=97; //ASCII码
c1='\u0001'; //UNICODE编码 十六进制
char smile = "😀";
smile.charAt(0);
char car = '𝄞';
char c1 = '𝌆';
String a="";
}
}
问题:
你可能会碰到这样的问题。比如发送短信,短信长度为 140 字节,如果文本超过了 140 个字节,你就必须将其截成多条。同时你又希望尽可能多的利用这 140 个字节。如果每个中文 2 字节,把短信内容限制在 70 个字符,那么英文较多的短信长度就被浪费了。你希望有一个方法,按字节来截取字符串,得到不超过 140 字节的最长子字符串。
-
运算符应用
package com.java.basic;
public class OperatorTests {
public static void main(String[] args) {
byte a=10;
short b=Short.MAX_VALUE;
b+a;
//a=a+10; //表达式自动类型提升
a+=10;
//=========
int b=10;
int c=b+++b+++b++;
int c=(b++)+(b++)+(b++);
System.out.println(b);//13
System.out.println(c);//33
//==========
int d=10,e=20;
int result=d>e?d++:e++;
System.out.println(result);
//&&、||
int t1=10,t2=20;
boolean flag=t1++>20&&t2--<20;
System.out.println(flag);
System.out.println(t2);//20
//&、>>> (位运算)
result=8/2;
result=8>>2;
System.out.println(result);
}
}
-
面试常见语句应用
package com.java.basic;
public class SwitchTests {
public static void main(String[] args) {
int a=5;
int result=0;
switch (a){//byte/short/int/char/String/enum
case 1: result+=10;
case 2: result+=20;
case 3: result+=30;
default:result+=40;
}
System.out.println(result);
}
}
-
数组定义案例
package com.java.basic;
public class ArrayTests {
public static void main(String[] args) {
//数组变量的定义
int[] a1;
int[][] a2;
int[]a3[];
int[][][] a4;
//数组的初始化
int[] b2={1,2,3};
//int[] b3;b3={2,3,4}; //error (这种形式的赋值只能发生在定义时)
int[] b4;b4=new int[]{2,3,4};
int[][] b5=new int[3][2];
//int[][] b6=new int[][2];//error (不能没有高位,只有低位)
int[]b7[]=new int[3][];
b7[0]=new int[2];
b7[1]=new int[3];
b7[2]=new int[1];
int[]b8[]={{1},{2,3}};
System.out.println(b8[1][1]);
}
}
-
方法参数传递
public class ParameterTests {
public static void main(String[] args) {
int[] array={1,2,3};
int a=10;
change(a,array);
System.out.println(a);//10
System.out.println(array[0]);//10
}
static void change(int a,int[] array){
++a;
array[0]=10;
}
}
JAVA集合应用
思维导图
技术图析
Hashmap原理分析
-
初始大小设计
HashMap 默认的初始大小是 16,当然这个默认值是可以设置的,如果事先知道大概的数据量有多大,可以通过修改默认初始大小,减少动态扩容(2的n次方)的次数,这样会大大提高 HashMap 的性能。
-
装载因子和动态扩容设计
最大装载因子默认是 0.75,当 HashMap 中元素个数超过 0.75*capacity(capacity表示散列表的容 量)的时候,就会启动扩容,每次扩容都会扩容为原来的两倍大小。
-
为什么扩容因子为0.75?
为什么不是0.5,也不是1呢?是因为这个0.75是在时间和空间上取的相对平衡值,假如在1的时候扩容,数组中数据越多,产生散列冲突的几率越大,一旦产生散列冲突数据就会转换为链表进行存储,而链表方式会影响查询效率. 假如在0.5时进行扩容,但又没有那么多元素要进行存储,可能会产生大量的空间浪费.
-
JDK8中HashMap 的扩容机制
1) 空参数的构造函数:实例化的 HashMap 默认内部数组是 null,即没有实例化。第一次调 用 put 方法时,则会开始第一次初始化扩容,长度为 16。
2) 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的 2 的幂数,将这个数设置赋值给阈值(threshold)。第一次调用 put 方法时,会将阈值赋值给容量, 然后让阈值 = 容量 x 负载因子。
3) 如果不是第一次扩容,则容量变为原来的 2 倍,阈值也变为原来的 2 倍。(容量和阈值都 变为原来的 2 倍时,负载因子还是不变)。
-
散列冲突及解决方案设计
HashMap 底层采用链表法来解决冲突。即使负载因子和散列函数设计得再合理,也免不了会出现 链表过长的情况,一旦出现链表过长,则会严重影响 HashMap 的性能。 于是,在 JDK1.8 版本中,为了对 HashMap 做进一步优化,官方引入了红黑树。而当链表长度太 长(默认超过 8)时,链表就转换为红黑树。我们可以利用红黑树快速增删改查的特点,提高 HashMap 的性能。当红黑树结点个数小于或等于6的时候,又会将红黑树转化为链表。因为在数据量 较小的情况下,红黑树要维护平衡,比起链表来,性能上的优势并不明显。
-
为什么是链表长度达到8的时,进行红黑树转换?
经过大量计算、测试,链表的长度达到8的几率已经很小,所以可以直接基于8作为链表转红黑树的边界值。
为什么不是大于呢,因为链表长度较长查询效率就会越低。为什么不是7呢?链表结点数量比较小时,应用
红黑树还要进行树的平衡设计,需要的成本相对比较高。
-
为什么红黑树节点个数小于6的时要转换链表呢?
假如是7则数据在链表和红黑树之间来回转换可能会比较频繁,这样就需要更长的时间消耗。
-
线程(thread)安全设计
HashMap本身并不是线程安全的对象,所以仅可以应用在线程安全的环境。在线程不安全的环境推荐使用ConcurrentHashMap,此map在JDK8中采用了CAS算法保证对map的操作是线程安全的;
ConcurrentHashMap
ConcurrentHashMap 的存储结构是怎样的?
1) Java7 中 ConcurrnetHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个 线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它 的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变,默认 Segment 的 个数是 16 个。
2) Java8 中的 ConcurrnetHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红 30 黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红 黑树,在冲突小于一定数量时又退回链表。
案例代码
基于LinkedHashMap演示Lru原理。
源码解析
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last
@Test
void testLinkedHashMap(){//Lru算法在LinkedHashMap中的应用
//LinkedHashMap满的时移除最近没有访问过的数据(查构造方法,分析对象构建)
int initialCapacity=3;
LinkedHashMap<String,String> map=
new LinkedHashMap<String,String>(initialCapacity,0.75f,true){
//系统在每次调用put方法向容器放数据,默认都会调用此方法
protected boolean removeEldestEntry(Map.Entry<String,String> eldest) {
return size()>initialCapacity;//true表示移除元素
}
};//true表示记录访问顺序
map.put("A","100");
map.put("B","200");
map.put("C","300");
map.get("A");
map.put("D","400");
System.out.println(map);
}
Java多线程应用
思维导图
技术图析
线程状态解析
Java中线程池ThreadPoolExecutor对象分析
Java中线程池的创建有多种方式,简单池的创建方式可以借助Executors中的一些静态方法创建,实际项目中用的最多就是基于ThreadPoolExecutor对象创建线程池,例如Tomcat处理请求时的线程对象就来自于这个对象创建的池,还有Spring Boot工程中异步操作的实现使用的就是这个线程池。
-
线程池参数有哪些?
-
corePoolSize 核心线程大小
-
maximumPoolSize 线程池最大线程数量。
-
keepAliveTime 空闲线程存活时间。
-
unit 空间线程存活时间单位。
-
workQueue 工作队列。
-
threadFactory 线程工厂。
-
handler 拒绝策略。
-
-
线程池的拒绝策略有哪些?
-
AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常 (默认拒绝策略)。
-
DiscardPolicy:丢弃任务,但是不抛出异常。
-
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被 拒绝的任务。
-
CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。
-
-
核心线程池大小如何设置?
-
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N (CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断, 或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而 在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而 线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程 使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
-
-
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单 凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相 比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
说说synchronized关键字底层锁升级是怎样的?
synchronized在JDK1.6以后,描述代码块或方法时,会默认提供对优化使用,随着并发数的增加它会有一个锁升级过程。
案例代码
案例1:wait/notify/notifyAll方法方法应用
基于wait/notify/notifyAll方法实现线程之间的通讯,这里的wait/notify/notifyAll方法必须用在同步方法或同步代码块内部,由对象锁调用。
@SpringBootTest
public class ThreadCommunicationTests {
String s1;
@Test
void test01() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
new Thread(()->{
synchronized (ThreadCommunicationTests.class) {
s1 = "Hello JSDTN2211";
//唤醒阻塞的线程
ThreadCommunicationTests.class.notifyAll();
}
}).start();
//main线程
synchronized (ThreadCommunicationTests.class) {
while (s1 == null) ThreadCommunicationTests.class.wait();
System.out.println(s1.toUpperCase());//NullPointerException
}
}
}
案例2:ThreadLocal的应用
基于ThreadLocal保证每个线程一个SimpleDateFormat对象(此对象线程不安全),ThreadLocal对象提供了这样的一种机制,能够将某个对象绑定到当前线程(调用set方法),也可以从当前线程获取某个对象(调用get方法)。
@SpringBootTest
public class ThreadLocalTests {
static class DateUtil{
//SimpleDateFormat是一个线程不安全的对象,不允许多线程共享,可以每个线程一份
//static SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<>();
public static Date parse(String str){
try {
//从当前线程获取SimpleDateFormat对象
SimpleDateFormat sdf =threadLocal.get();//ThreadLocalMap.get(threadLocal)
if(sdf==null) {
System.out.println("new SimpleDateFormat()");
//创建SimpleDateFormat对象
sdf = new SimpleDateFormat("yyyy/MM/dd");
//将SimpleDateFormat对象绑定到当前线程
threadLocal.set(sdf);//ThreadLocalMap.set(threadLocal,sdf)
}
return sdf.parse(str);
}catch (ParseException e){
e.printStackTrace();
return null;
}
}
}
@Test
void testParse(){
Thread t1=new Thread(()->{
for(int i=0;i<5;i++){
DateUtil.parse("2023/05/18");
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5;i++){
DateUtil.parse("2023/05/18");
}
});
t1.start();
t2.start();
for(int i=0;i<5;i++){
DateUtil.parse("2023/05/18");
}
}
}
基于volatile,synchronized实现双重校验锁单例
volatile 主要用于描述类中的属性 1)保证线程可见性(对变量的修改可以直接同步给线程) 2)禁止指令重排序(JVM执行时,考虑执行效率,可能会调整指令执行顺序)
public class SingletonTests {
static class Singleton{//双重校验单例
private Singleton(){}
/**
* volatile 主要用于描述类中的属性
* 1)保证线程可见性(对变量的修改可以直接同步给线程)
* 2)禁止指令重排序(JVM执行时,考虑执行效率,可能会调整指令执行顺序)
*/
private static volatile Singleton instance;
public static Singleton getInstance(){
if(instance==null){//A,B,C
synchronized (Singleton.class){
if(instance==null)//单例双重校验法
instance=new Singleton();
//1)分配空间
//2)属性默认初始化
//3)调用构造方法
//4)将对象地址赋值给instance变量
}
}
return instance;
}
}
}
i++
Java反射技术
思维导图
案例分析
通过反射技术理解泛型类型擦除
@Test
void testGeneric() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ArrayList<String> list=new ArrayList<>();
list.add("A");
list.add("B");
//list.add(100);编译时无法添加
//请运用反射技术将100这个整数添加到list集合
//获取类的字节码对象
Class<?> cls=list.getClass();
//获取ArrayList对象的add方法
Method addMethod = cls.getDeclaredMethod("add", Object.class);
//通过反射执行方法
addMethod.invoke(list,100);
System.out.println(list);
}
通过反射技术操作注解
反射技术与注解的应用。
public class AnnotationTests {
//模拟Knife4J中的一个定义方法参数的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ApiImplicitParam{//一个Interface,默认继承Annotation接口
String name();
String value();
String dataType();
}
//模拟一个Controller类型
class XxxController{
@ApiImplicitParam(name = "page", value = "页码", dataType = "int")
public void doSelect(){}
}
//通过反射技术获取XxxController类中doSelect()方法上的注解以及注解中方法的值
public static void main(String[] args) throws NoSuchMethodException {
Class<XxxController> xxxControllerClass = XxxController.class;
Method doSelect = xxxControllerClass.getDeclaredMethod("doSelect");
ApiImplicitParam annotation = doSelect.getAnnotation(ApiImplicitParam.class);
String name = annotation.name();
String value=annotation.value();
String dataType = annotation.dataType();
System.out.println(name+"/"+value+"/"+dataType);
}
}
Java技术新特性
JDK5新特性
JDK5新特性,后续文章更新,也可查阅官方文档。
JDK8新特性
JDK5新特性,后续文章更新,也可查阅官方文档。