Java面试题
陆陆续续收集到的一些Java面试题知识点,后续加更
基础
1什么是逃逸分析
指JVM可以分析新创建对象的使用范围,以此来决定是否在Java堆上分配内存的技术
逃逸分析使用的算法引入了连通图,来构建对象和对象引用之间的可达性关系,以此为基础提出一种组合数据流分析法。
这种算法是上下文相关和流敏感的,分析精度相对较高,但是时间、内存开销相对较大。
逃逸的几种状态
-
全局逃逸
即一个对象的作用范围逃出了当前方法或当前范围,有以下几种:
- 对象是静态变量
- 对象作为当前方法的返回值
- 对象是一个已经逃逸的对象
-
参数逃逸
即一个对象被作为方法参数传递或被参数引用,但不会发生全局逃逸,这个状态通过被调用方法的字节码确定
-
没有逃逸
逃逸分析优化
-
锁清除
线程同步锁是很牺牲性能的。锁清除就是如果编译器知道当前对象只有当前线程使用,那么就会移除该对象的同步锁。
比如StringBuffer和Vector都是用synchronized修饰线程安全的,但是大部分时候只在当前线程中使用,故编译器可以优化它
-
标量替换
标量和聚合量:
基础类型和对象引用可以说是标量,不能被进一步分解;能进一步分解的量就是聚合量,比如对象。
对象可以被进一步分解为标量,分散的成员变量,这就是标量替换
如果一个对象没有逃逸,那么不用创建它,只需要创建它的成员标量并存在栈中或者寄存器,节省内存空间也提升性能。
-
栈上分配
当对象没有逃逸时,该对象可以通过分解为成员标量分配在栈内存,和方法的生命周期一致,随着栈帧出栈销毁,从而减少GC的压力,提高性能。
逃逸分析即是为了优化JVM内存和提升性能的。故开发当中要尽可能控制变量的作用域,作用域越小越好,
2 ==和equals()的区别
==:如果比较基本数据类型,比较数值是否相等
如果比较引用类型,比较对象的地址是否相等
equals():默认比较对象地址是否相等,不能用于比较基本数据类型
为什么用equals()比较String的内容是否相等
对于String、Date、Integer灯类型重写了equals()方法,使其比较对象存储的内容是否相等
3 &和&&的区别?
& 可以作位运算符,进行位与运算;还可以进行逻辑与运算,作逻辑与时为长路与,即是说就算左边的表达式为假,右边的表达式也会运算
&& 是逻辑与,是短路与,若左边表达式为假,则右边表达式不运算
4 怎么理解值传递和引用传递?
值传递:形参传递的是基本数据类型的字面量值的拷贝,方法对形参的修改不影响实参的值
引用传递:形参传递的是该参数引用的对象在堆中地址值的拷贝,对形参的修改直接作用在实参
5 static可以修饰局部变量么?
不能,可以是内部类、全局变量、方法、代码块
6 私有方法可以重载或者重写吗?
可以重载,不能重写
7 String可变吗?
不可变,String是final类型的,其值value是char[],而且是private final的,故不可修改
8 transient关键字的作用
被transient修饰的变量不能被序列化
transient只作用于实现了Serializable接口的类
transient只能用来修饰普通成员变量字段
不管有无transient修饰,静态变量都不能被序列化
9 Class.forName和ClassLoader的区别
Class.forName除了将类的.class文件加载到JVM之外,还可以对类进行初始化
ClassLoader只会将.class文件加载到JVM中,不会进行初始化
10 main方法可以重载或者重写吗?
可以重载,但是JVM始终调用原始的main方法,不会调用重载的main
不能重写。因为main方法是static的,在Java中不能被覆盖
11 throw 和throws的区别
throw是真实抛出异常,并且throw是动作,代表抛出了异常
throws是声明可能抛出异常,表示一种抛出异常的可能性
12 int和Integer的区别
Integer是int的包装类,int则是Java的基本数据类型
Integer必须实例化在能使用,int不需要
Integer是对象引用,new一个Integer时,实际上生成一个指针指向此对象,而int直接存储数据值
Integer默认值是null,int默认是0
13 switch case语句
case里必须跟break,否则会一个一个case执行下去,直到最后一个break的case或default出现
case条件里只能是常量或者字面常量
default可有可无,最多有一个
switch支持类型:
基本数据类型:byte short int char
包装类型:上面四个的
枚举类型: Enum
字符串类型 String
14 不能用➕拼接字符串的时候:
通过多个表达式完成一个字符串拼接时不行
一次性拼接一个字符串时就可以用➕
15 Java金额计算怎么避免精度丢失?
金额运算尽量使用BigDecimal(String val)进行计算
数据库存储金额,一般是整形和浮点型两种,如果有汇率转换,建议用decimal进行存储,可以灵活控制精度,decimal直接对应Java 类型BigDecimal。
16 怎么理解Java的类型提升?
所谓类型提升,就是在含有多种数据类型的表达式中,类型会自动向范围表示大的数据类型提升。比如:
long count=100000000;
int price=1999;
long totalPrice=price*count; //运算结果为long型,没有溢出
17 String有没有长度限制?
有,65534个字节,超过的话编译报错
18 Java语法糖是什么意思?
也称糖衣语法,指在计算机语言中添加的某种语法,对语言本身功能没有影响,只是为了便于程序员开发,提高效率。就是对现有语法的封装。
Java语法糖主要有:
泛型与类型擦除
自动装箱与拆箱
变长参数
增强for循环
内部类
枚举类
19 transient关键字的作用
- transient修饰的变量不能被序列化
- transient只作用于实现Serializable接口
- transient只能用来修饰普通成员变量字段
- 不管有无transient修饰,静态变量不能被序列化
20 如何实现对象克隆?
可以通过实现Cloneable接口,然后重写其clone()方法
21 Java8 添加的新特性
- Lambda表达式
- 函数式接口
- 接口默认方法和静态方法
- Optional类
- 重复注解
- BASE64编码解码已经加入jdk8
- JVM内存取消永久代
- …
22 String、StringBuffer、StringBuilder有什么区别?
String、StringBuffer、StringBuilder最大的不同是String不可变,后者可变。StringBuffer是线程安全的,StringBuilder线程不安全速度较快。
23 String与byte[]两者相互之间如何转换?
String > byte[] 通过String类的getBytes方法;byte[] > String通过new String(byte[])构造器。
24 普通类和抽象类有什么区别?
- 普通类不能包含抽象方法,抽象类可以
- 抽象类不能直接实例化,普通类可以直接实例化
25 throw和throws的区别
1位置不同:throws用在函数上,后面跟的是异常类,可以跟多个
throw用在函数内,后跟异常对象
2作用不同:
throws用来声明异常,让调用者知道可能发生的异常,预先做好处理准备
throw抛出具体的异常对象,执行到throw语句,那么功能也就结束了,跳转到调用者,并把具体异常抛给调用者
(throw语句独立存在的话后面就不应该跟别的语句了,因为根本不会执行)
throws表示可能发生某种异常,但是实际上并不一定会发生,而throw语句如果被执行了,就一定是抛出异常给调用者。
两者都是消极的异常处理方式,不会由出现异常的函数去处理异常,而是让调用者处理异常。区别就是一个告诉你可能抛出异常,另一个抛出异常
26 Java反射机制的概念
指在运行状态中,对于任意一个类都可以知道这个类的所有属性和方法,并且对于任意一个对象,都能调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
27 获取类对象的三种方法
1 调用某个对象的getClass()方法
Person person = new Person();
Class clazz = p.getClass();
2 调用某个类的class属性来获取其类对象
Class clazz = Person.class;
3 使用CLass类中的forName() 静态方法(最安全,性能最好)
Class clazz = CLass.forName("类的全路径")(最常用)
获得了类对象后,可以通过Class类的方法获取并查看类中的方法和属性
//获取Person类的Class对象
Class clazz=Class.forName("reflection.Person");
//获取Person类的所有方法信息
Method[] method=clazz.getDeclaredMethods();
for(Method m:method){
System.out.println(m.toString());
}
//获取Person类的所有成员属性信息
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
System.out.println(f.toString());
}
//获取Person类的所有构造方法信息
Constructor[] constructor=clazz.getDeclaredConstructors();
for(Constructor c:constructor){
System.out.println(c.toString());
}
28
集合
集合使用泛型有什么优点?
- Java1.5引入泛型,所有集合接口和实现都大量使用它
- 泛型允许我们为集合提供一个可以容纳的对象类型,如果添加别的类型,编译会报错
- 避免在运行时出现ClassCastException
- 泛型使得代码整洁,不需要使用显示转换和instanceOf操作符
- 对运行时有好处,因为不会产生类型检查的字节码指令
常见集合有哪些
- Collection接口的子接口包括:Set接口和List接口
- Map接口的实现类:HashMap、TreeMap、Hashtable、ConcurrentHashMap等
- Set接口的实现了有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类有:ArrayList、LinkedList、Stack、Vector等
常用的线程安全的Map
Java中平时用的最多的Map集合就是HashMap了,它是线程不安全的。
举例两个场景:
1、当用在方法内的局部变量时,局部变量属于当前线程级别的变量,其他线程访问不了,所以这时也不存在线程安全不安全的问题了。
2、当用在单例对象成员变量的时候呢?这时候多个线程过来访问的就是同一个HashMap了,对同个HashMap操作这时候就存在线程安全的问题了。
线程安全的Map
为了避免出现场景2的线程安全的问题,不能使用HashMap作为成员变量,要寻求使用线程安全的Map,下面来总结下有哪些线程安全的Map呢?
1、HashTable
private Map<String, Object> map = new Hashtable<>();
HashTable的源码
HashTable的get/put方法都被synchronized关键字修饰,说明它们是方法级别阻塞的,它们占用共享资源锁,所以导致同时只能一个线程操作get或者put,而且get/put操作不能同时执行,所以这种同步的集合效率非常低,一般不建议使用这个集合。
2、SynchronizedMap
private Map<String, Object> map = Collections.synchronizedMap(new HashMap<String, Object>());
这种是直接使用工具类里面的方法创建SynchronizedMap,把传入进行的HashMap对象进行了包装同步而已,来看看它的源码。
这个同步方式实现也比较简单,看出SynchronizedMap的实现方式是加了个对象锁,每次对HashMap的操作都要先获取这个mutex的对象锁才能进入,所以性能也不会比HashTable好到哪里去,也不建议使用。
3、ConcurrentHashMap - 推荐
private Map<String, Object> map = new ConcurrentHashMap<>();
这个也是最推荐使用的线程安全的Map,也是实现方式最复杂的一个集合,每个版本的实现方式也不一样,在jdk8之前是使用分段加锁的一个方式,分成16个桶,每次只加锁其中一个桶,而在jdk8又加入了红黑树和CAS算法来实现。
虽然实现起来很复杂,但使用起来也是非常简单的,在java面试中问的频率也非常高,最重要的是性能要比上面两种同步方式要快太多,推荐使用。
HashMap的数据结构?
JDK1.7:数组➕链表
JDK1.8:数组➕链表➕红黑树(链表长度>8则转换为红黑树)
HashMap如何扩容?
先了解一些HashMap中的变量:
- Node<K,V>:链表节点,包含了key、value、hash、next指针四个元素
- table:Node<K,V>类型的数组,里面的元素是链表,用于存放HashMap元素的实体
- size:记录了放入HashMap的元素个数
- loadFactor:负载因子
- threshold:阈值,决定了HashMap何时扩容,以及扩容后的大小,一般等于table大小乘以loadFactor
- table数组大小有capacity参数确定,默认16,也可以构造时传入,最大限制是1<<30
- 装载因子 loadFactor用来确认table数组是否需要动态扩展,默认0.75,比如table大小为16时,threshold就是12,如果table实际大小超过12,就动态扩容
- 扩容调用resize()方法,将table长度变为两倍
- 数据很大时,扩展会带来性能损失,如果性能要求很高,那么这种损失影响很大
ArrayList(数组实现,线程不安全)
ArrayList最常用的List实现类,内部采用数组实现,允许随机访问集合元素。每个元素之间不允许有间隔,当数组大小不足时需要增加存储能力,将原数组复制到新空间。插入、删除元素时需要移动其他数组元素,代价较高。适合随机查找和遍历,不适合插入删除
Vector(数组实现、线程同步,即线程安全)
Vector相比于ArrayList来说支持线程同步,某一时刻只能有一个线程能写Vector,避免多线程同时写引起的不一致性,但实现同步需要较高代价,因此Vector相对ArrayList操作慢
LinkedList(链表实现)
适合动态插入删除数据,但随机访问、插入删除的速度较慢,提供了List没有定义的方法,专门用来操作表头表尾元素,可以当做堆栈、队列和双向队列使用
Set
Set强调独一无二,即集合中的元素不允许重复,无序存储(存入和取出的顺序未必相同),值不能重复。
对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖Object的hashCode方法和equals方法。
HashSet(哈希表)
哈希表存放哈希值,按照哈希值的顺序来存取对应的数据。元素的哈希值是通过元素的hashcode方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法如果equls结果为true,HashSet就视为同一个元素。如果equals为false就不是同一个元素。哈希值相同equals为false的元素就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希值一样的存一列,组成一个链表的意思,可以了解一下数据结构课程上学到的链地址法
TreeSet(二叉树)
1.TreeSet使用二叉树对新add()的元素按照指定顺序排序,每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
2.Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable接口,并且覆写相应的compareTo()函数,才可以正常使用。
3.在覆写compare()函数时,要返回相应的值才能使TreeSet按照一定的规则来排序
4.比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
JVM相关
基本概念
JVM是可以运行Java代码的假想计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆和一个存储方法域。JVM运行在操作系统之上,与硬件没有直接的交互。每种操作系统都对应于自己特定的JVM。
一般讲的“Java文件可以一次编译,到处运行”就是因为:同一份Java源文件通过编译器,能够生产相应的.Class文件,也就是字节码文件,字节码文件是统一格式的,不管拿到哪一种操作系统上的Java虚拟机,字节码文件通过Java虚拟机中的解释器,编译成特定机器上的机器码,就可以在特定机器上运行了。每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这就是Java为什么能够跨平台的原因。
进程与线程
没有引入线程以前,进程是程序的一次动态执行过程,是操作系统资源分配与调度的基本单位。
引入线程后,进程仍然是资源分配的基本单位,但是线程才是处理机调度的基本单位。
进程之间的地址空间互相隔绝,不能越界访问;而线程由进程产生,同一个进程的各个线程共享该进程的内存地址空间资源。
Java线程
Hotspot JVM 中的Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程(计算机考研说的内核线程)。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的CPU 上。当原生线程初始化完毕,就会调用Java 线程的run() 方法。run() 方法运行完毕,线程就结束了。当线程结束时,会释放原生线程和Java 线程的所有资源。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxSPtmFB-1614667873005)(Java面试题.assets/image-20210302144123188.png)]
JVM内存
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA堆、方法区】、直接内存。
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁(在Hotspot VM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的生存情况跟随本地线程的生存情况对应)。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
直接内存并不是JVM运行时数据区的一部分,但也会被频繁的使用: 在JDK 1.4引入的NIO提供了基于Channel与Buffer的IO方式, 它可以使用Native函数库直接分配堆外内存, 然后使用DirectByteBuffer对象作为这块内存的引用进行操作(详见:Java I/O 扩展), 这样就避免了在Java堆和Native堆中来回复制数据, 因此在一些场景中可以显著提高性能
WEB
http和https区别?
- http是简单的无状态的协议,https是http➕SSL组合的,可以进行加密传输、身份验证的更为安全的协议
- 端口号不同,http为80,https为443
forward和redirect区别?
- forward是转发,redirect是重定向
- 使用forward,浏览器地址栏的url不会变,而使用rediret则会变化
- forward可以共享request里的数据,而redirect则不行
- forward比redirect效率更高
Servlet是什么?
是用Java编写的服务器端程序,主要功能是交互式得浏览、修改数据,生成动态web内容
狭义的servlet是java的一个借口,广义的是实现了servlet家口的类,一般理解是后者