Java高频面试题解析,直戳面试官痛点,多家互联网大厂Offer等你拿
Posted Java小叮当
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高频面试题解析,直戳面试官痛点,多家互联网大厂Offer等你拿相关的知识,希望对你有一定的参考价值。
前言
回顾多灾多难的2021年,新冠疫情持续肆虐全球,疫情确诊曲线起伏跌宕,由此引发一系列事件:经济萎缩、 财政刺激、疫苗研发、经济复苏等等。无不牵动着市场的神经。“后疫情时代”将重新定义2022年,新旧交接之际,把握机遇。珍惜当下。
关于面试,特整合了Java面试题,希望对找工作、跳槽的小伙伴有所帮助。总之一句话越基础的东西越重要,很多人认为自己会用它们写代码了,其实仅仅是知道如何调用api而已,离会用还差的远。互联网发展迅速的时代,只能跟上时代的进步,才不会被淘汰。
Java基础
1、List和Set的区别。
2、HashSet是如何保证不重复的?
3、HashMap是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?
4、HashMap的扩容过程。
5、HashMap1.7与1.8的 区别,说明1.8做了哪些优化,如何优化的?
6、final finally finalize。
7、强引用 、软引用、 弱引用、虚引用。
8、Java反射。
9、Arrays.sort 实现原理和Collection实现原理。
10、LinkedHashMap的应用。
Java 并发
1、synchronized的实现原理以及锁优化?
2、volatile的实现原理?
3、Java的信号灯?
4、synchronized在静态方法和普通方法的区别?
5、怎么实现所有线程在等待某个事件的发生才会去执行?
JVM
1.JVM的内存结构。
2.JVM方法栈的工作过程,方法栈和本地方法栈有什么区别?
3.JVM的栈中引用如何和堆中的对象产生关联。
4.可以了解一下逃逸分析技术。
5.GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
6.标记清除和标记整理算法的理解以及优缺点。
7.eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。
8.JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型?
9.强软弱虚引用的区别以及GC对他们执行怎样的操作?
10.Java是否可以GC直接内存。
这些问题都是抽取了部分发出来,答案解析和知识点都整理在Java文档里了,详细内容有很多,为了不影响阅读,可看整理的《Java架构进阶面试题手册》。
Spring
1、BeanFactory和FactoryBean?
2、Spring IOC的理解,其初始化过程?
3、BeanFactory和ApplicationContext?
4、Spring Bean的生命周期,如何被管理的?
5、Spring Bean的加载过程是怎样的?
6、如果要你实现Spring AOP,请问怎么实现?
7、如果要你实现Spring IOC,你会注意哪些问题?
8、Spring是如何管理事务的,事务管理机制?
SpringBoot-Spring Cloud
1、什么是Spring Boot?
2、SpringBoot有哪些优点?
3、什么是JavaConfig?
4、4、如何重新加载Spring Boot上的更改,而无需重新启动服务器?
5、Spring Boot中的监视器是什么?
6、如何在Spring Boot中禁用Actuator端点安全性?
7、如何在自定义端口上运行Spring Boot应用程序?
8、什么是YAML?
9、如何实现Spring Boot应用程序的安全性?
10、如何集成Spring Boot和ActiveMQ?
Redis
1.RDB和AOF区别。
2.为什么RDB要fork子进程而不是线程。
3.redis基本数据类型。
4.zset的底层数据结构,跳表何时增加高度。
5.分布式redis,缓存和数据库读写不一致,答延迟双删。
mysql高频20题解析
1.事务四大特性(ACID)原子性、一致性、隔离性、持久性?
2.事务的并发?事务隔离级别,每个级别会引发什么问题,MySQL默认是哪个级别?
3.MySQL常见的三种存储引擎(InnoDB、MyISAM、MEMORY)的区别?
4.MySQL的MyISAM与InnoDB两种存储引擎在,事务、锁级别,各自的适用场景?
5.查询语句不同元素(where、jion、limit、group by、having等等)执行先后顺序?
多线程
1.Java实现多线程有哪几种方式?
2.Callable和Future的了解。
3.线程池的参数有哪些,在线程池创建一个线程的过程。
4.volitile关键字的作用,原理。
5.synchronized关键字的用法,优缺点。
Netty
1.Netty 是什么?
2.Netty 的特点是什么?
3.Netty 的优势有哪些?
4.Netty 的应用场景有哪些?
5.Netty 高性能表现在哪些方面?
ZooKeeper
1.ZooKeeper是什么?
2.ZooKeeper提供了什么?
3.Zookeeper文件系统。
4.Zookeeper怎么保证主从节点的状态同步?
5.四种类型的数据节点Znode。
Tomcat
1.Tomcat是什么?
2.Tomcat的缺省端口是多少,怎么修改?
3.tomcat有哪几种Connector运行模式(优化)?
4.Tomcat有几种部署方式?
5.tomcat容器是如何创建servlet类实例?用到了什么原理?
解决方案篇
- API接口安全设计
- 秒杀系统设计思路
- 分布式事务解决方案
- SSO单点登录方案
- Redis缓存和MySQL数据一致性方案详解
- 分库分表设计
- 缓存雪崩,穿透,击穿解决方案
面向对象
什么是面向对象?
对比面向过程,是两种不同的处理问题的角度面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么
比如:洗衣机洗衣服面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机----->2、放衣服----->3、放洗衣粉----->4、清洗 >5、烘干面向对象会拆出人和洗衣机两个对象: 人:打开洗衣机 放衣服 放 洗 衣 粉 洗衣机:清洗 烘干从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护
面向对象
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1、javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决 定。而不能由外部胡乱修改该name有自己的命名规则,明显不能由外部直接赋值
private String name;
public void setName(String name) this.name = "tuling_"+name;
2、orm框架
操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可。
继承:继承基类的方法,并做出自己的改变和/或扩展子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。 继承,方法重写,父类引用指向子类对象,父类类型 变量名 = new 子类对象 ;变量名.方法名();无法调用子类特有的功能。
JDK JRE JVM
JDK:
Java Develpment Kit java 开发工具
JRE:
Java Runtime Environment java运行时环境JVM:
java Virtual Machine java 虚拟机
==和equals比较
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equals:object中默认也是采用==比较,通常会重写Object
public boolean equals(Object obj) return (this == obj);
String
上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。
public boolean equals(Object anObject) if (this == anObject)
return true;
if (anObject instanceof String)
String anotherString = (String)anObject; int n = value.length;
if (n == anotherString.value.length) char v1[] = value;
char v2[] = anotherString.value; int i = 0;
while (n-- != 0)
if (v1i != v2i) return false;
i++;
return true;
return false;
public class StringDemo
public static void main(String args[]) String str1 = "Hello";
String str2 = new String("Hello"); String str3 = str2; // 引用传递
System.out.println(str1 == str2); // false System.out.println(str1 == str3); // false System.out.println(str2 == str3); // true System.out.println(str1.equals(str2)); // true System.out.println(str1.equals(str3)); // true System.out.println(str2.equals(str3)); // true
hashCode与equals
hashCode介绍:
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用 到了散列码!(可以快速找到所需要的对象)
为什么要有hashCode:
以“HashSet如何检查重复”为例子来说明为什么要有hashCode:
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有 值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来 检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
如果两个对象相等,则hashcode一定也是相同的两个对象相等,对两个对象分别调用equals方法都返回true 两个对象有相同的hashcode值,它们也不一定是相等的因此,equals方法被覆盖过,则hashCode方法也必须被覆盖hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个 对象无论如何都不会相等(即使这两个对象指向相同的数据)
final
最终的
修饰类:表示类不可被继承
修饰方法:表示方法不可被子类覆盖,但是可以重载修饰变量:表示变量一旦被赋值就不可以更改它的值。
- 修饰成员变量
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
- 修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时, 即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码 中对final变量赋初值(仅一次)
public class FinalVar
final static int a = 0;//再声明的时候就需要赋值 或者静态代码块赋值
/** static
a = 0;
*/
final int b = 0;//再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值
/*
b = 0;
*/
public static void main(String[] args)
final int localA; //局部变量只声明没有初始化,不会报错,与final无关。localA = 0;//在使用之前一定要赋值
//localA = 1; 但是不允许第二次赋值
- 修饰基本类型数据和引用类型数据
如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变 的。
public class FinalReferenceTest public static void main()
final int[] iArr=1,2,3,4; iArr2=-3;//合法
iArr=null;//非法,对iArr不能重新赋值
final Person p = new Person(25);
p.setAge(24);//合法
p=null;//非法
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会生成两个class文件,Test.class Test1.class
public class Test
public static void main(String[] args)
//局部final变量a,b
public void test(final int b) //jdk8在这里做了优化, 不用写,语法糖,但实际上也是有的,也不能修改
final int a = 10;
//匿名内部类new Thread()
public void run() System.out.println(a); System.out.println(b);
;
.start();
class OutClass
private int age = 12;
public void outPrint(final int x) class InClass
public void InPrint()
System.out.println(x); System.out.println(age);
new InClass().InPrint();
首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有 没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解 决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修 改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
String、StringBuffer、StringBuilder
String是final修饰的,不可变,每次操作都会产生新的String对象StringBuffer和StringBuilder都是在原对象上操作StringBuffer是线程安全的,StringBuilder线程不安全的StringBuffer方法都是synchronized修饰的性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时使用后面两个优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
重载和重写的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方 法。
public int add(int a,String b) public String add(int a,String b)
//编译报错
接口和抽象类的区别
抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法。
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。抽象类只能继承一个,接口可以实现多个。
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有 什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无, 但不对如何实现行为进行限制。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实 现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己 实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是 is a 的关系,比如: BMW is a Car 。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是 like a 的关系。比如: Bird like a Aircraft (像飞行器一样可以飞),但其本质上 is a Bird 。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
**使用场景:**当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也 是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功 能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计 阶段会降低难度。
List和Set的区别
**List:**有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出 所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素
**Set:**无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元 素,在逐一遍历各个元素
ArrayList和LinkedList区别
**ArrayList:**基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固 定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会 涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚 至超过linkedList(需要创建大量的node对象)
**LinkedList:**基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需 要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结 果为空时会遍历整个列表。
HashMap和HashTable有什么区别?其底层实现是什么?
区别 :
- HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
- HashMap允许key和value为null,而HashTable不允许
2.底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表key为null,存在下标0的位置
数组扩容
ConcurrentHashMap原理,jdk7和jdk8版本的区别
jdk7:
数据结构: ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询: 二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁: Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment
get方法无需加锁,volatile保证jdk8:
数据结构: synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS。
锁: 锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见数组用volatile修饰,保证扩容时被读线程感知。
什么是字节码?采用字节码的好处是什么?
java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系 统的机器码执行。在Java中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节 码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机 器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器 >机器可执行的二进制机器码 >程序运行。
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器, 因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Java中的异常体系
Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检 查异常。
RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
Java类加载器
JDK自带有三个类加载器: bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class 类 。 AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。系统类加载器,线程上下 文加载器继承ClassLoader实现自定义类加载器
双亲委托模型
双亲委派模型的好处:
主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类
GC如何判断对象可以被回收
引用计数法: 每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计 数为0时可以回收,
面向对象
什么是面向对象?
对比面向过程,是两种不同的处理问题的角度面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么
比如:洗衣机洗衣服面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机----->2、放衣服----->3、放洗衣粉----->4、清洗 >5、烘干面向对象会拆出人和洗衣机两个对象: 人:打开洗衣机 放衣服 放 洗 衣 粉 洗衣机:清洗 烘干从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护
面向对象
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1、javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决 定。而不能由外部胡乱修改该name有自己的命名规则,明显不能由外部直接赋值
private String name;
public void setName(String name) this.name = "tuling_"+name;
2、orm框架
操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可。
继承: 继承基类的方法,并做出自己的改变和/或扩展子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
多态: 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。 继承,方法重写,父类引用指向子类对象,父类类型 变量名 = new 子类对象 ;变量名.方法名();无法调用子类特有的功能。
JDK JRE JVM
JDK:
Java Develpment Kit java 开发工具
JRE:
Java Runtime Environment java运行时环境JVM:
java Virtual Machine java 虚拟机
==和equals比较
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equals:object中默认也是采用==比较,通常会重写Object
public boolean equals(Object obj) return (this == obj);
String
上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。
public boolean equals(Object anObject) if (this == anObject)
return true;
if (anObject instanceof String)
String anotherString = (String)anObject; int n = value.length;
if (n == anotherString.value.length) char v1[] = value;
char v2[] = anotherString.value; int i = 0;
while (n-- != 0)
if (v1i != v2i) return false;
i++;
return true;
return false;
public class StringDemo
public static void main(String args[]) String str1 = "Hello";
String str2 = new String("Hello"); String str3 = str2; // 引用传递
System.out.println(str1 == str2); // false System.out.println(str1 == str3); // false System.out.println(str2 == str3); // true System.out.println(str1.equals(str2)); // true System.out.println(str1.equals(str3)); // true System.out.println(str2.equals(str3)); // true
hashCode与equals
hashCode介绍:
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用 到了散列码!(可以快速找到所需要的对象)
为什么要有hashCode:
以“HashSet如何检查重复”为例子来说明为什么要有hashCode:
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有 值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来 检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
如果两个对象相等,则hashcode一定也是相同的两个对象相等,对两个对象分别调用equals方法都返回true 两个对象有相同的hashcode值,它们也不一定是相等的因此,equals方法被覆盖过,则hashCode方法也必须被覆盖hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个 对象无论如何都不会相等(即使这两个对象指向相同的数据)
final
最终的
修饰类:表示类不可被继承
修饰方法:表示方法不可被子类覆盖,但是可以重载修饰变量:表示变量一旦被赋值就不可以更改它的值。
修饰成员变量
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时, 即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码 中对final变量赋初值(仅一次)
public class FinalVar
final static int a = 0;//再声明的时候就需要赋值 或者静态代码块赋值
/** static
a = 0;
*/
final int b = 0;//再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值
/*
b = 0;
*/
public static void main(String[] args)
final int localA; //局部变量只声明没有初始化,不会报错,与final无关。localA = 0;//在使用之前一定要赋值
//localA = 1; 但是不允许第二次赋值
- 修饰基本类型数据和引用类型数据
如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变 的。
public class FinalReferenceTest public static void main()
final int[] iArr=1,2,3,4; iArr2=-3;//合法
iArr=null;//非法,对iArr不能重新赋值
final Person p = new Person(25);
p.setAge(24);//合法
p=null;//非法
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会生成两个class文件,Test.class Test1.class
public class Test
public static void main(String[] args)
//局部final变量a,b
public void test(final int b) //jdk8在这里做了优化, 不用写,语法糖,但实际上也是有的,也不能修改
final int a = 10;
//匿名内部类new Thread()
public void run() System.out.println(a); System.out.println(b);
;
.start();
class OutClass
private int age = 12;
public void outPrint(final int x) class InClass
public void InPrint()
System.out.println(x); System.out.println(age);
new InClass().InPrint();
首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有 没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解 决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修 改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
String、StringBuffer、StringBuilder
String是final修饰的,不可变,每次操作都会产生新的String对象StringBuffer和StringBuilder都是在原对象上操作StringBuffer是线程安全的,StringBuilder线程不安全的StringBuffer方法都是synchronized修饰的性能:StringBuilder > StringBuffer > String
场景: 经常需要改变字符串内容时使用后面两个优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
重载和重写的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
public int add(int a,String b) public String add(int a,String b以上是关于Java高频面试题解析,直戳面试官痛点,多家互联网大厂Offer等你拿的主要内容,如果未能解决你的问题,请参考以下文章
全精品~ 2023年互联网大厂高频Java面试真题集锦(含答案解析)
查漏补缺:备战2021年java后端Kafka高频面试题(含答案解析)
2022 Android大厂高频面试题解析大全;Android面试官:“我需要领这份资料......”
阿里云/京东/百度等20多家大厂Java面试上千道内卷真题+简历模板