[Java专题] Java基础面试题(2022版本)

Java语言有哪些特点

● 简单易学
● 面向对象(封装、继承、多态)
● 平台无关性(Java虚拟机实现平台无关性)
● 支持多线程
● 编译和解释并行

Java特点要和C++进行对比,最好从编译型语言和解释型语言的角度来探讨,了解不同语言之间的适用场景

Java和C++的区别?
● 都是面向对象的语言,都支持封装、继承、多态。
● C++支持多继承,Java只能是单继承,利用接口实现多继承。
● C++需要进行内存管理,Java实现自动内存管理
● Java没有指针概念,而C++有指针概念,不是特别安全(容易造成内存泄漏和野指针出现)
○ 多个指针指向同一块内存,某个指针将内存释放,别的指针不知道(野指针)
○ delete 之前如果存在抛异常,那么也会导致内存泄漏

int main(){
	int *p = new int;
	*p = 1;
	p = new int; // 未释放之前申请的资源,导致内存泄漏 
	delete p;
	
	return 0;
}

● 野指针的概念:野指针就是指针指向的位置不可知的,野指针的三种情况:
○ 指针未定义

int* p;

指针越界访问

int arr[10]={0];
int* p=arr;
for(int i=0;i<12;i++)
{
	*p++=i;
}
return 0;

○ 指针指向的空间释放

int* test()
{
	int a=10;
	return &a;
}
int main()
{
	int* p=test();
	printf("%d\n",*p);
	return 0;
}

C++还涉及重载运算符,而Java是不存在的,这与Java最初的设计思想不符。

编译型和解释型语言的区别?
● 编译型:编译型语言会通过编译器将源代码一次性编译成该平台执行的机器码。执行效率快,开发效率低。
● 解释型:解释型语言会通过解释器一句一句地将代码解释为机器代码后执行。执行效率慢,开发效率高。

Java关键字

Java关键字

很多公司会问你基础数据类型(8种),然后会问你他们所占有的字节数,你还可以衍生出包装类,进而衍生出包装类常量池

Java基础类型的包装类的大部分都实现了常量池技术,需要记住的是Byte、Short、Integer、Long这4种包装类默认是在【-128,127】缓存区域,而Character默认缓存区域是在[0,127]缓存区域,Boolean直接返回True或False。

基础类型和包装类型的转换:
● 装箱:将基本类型用他们对应的引用类型包装起来
● 拆箱:将包装类型转换为基本数据类型

Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i 等价于 int n = i.intValue();

基础类型和包装类型有什么区别?
包装类型默认是NULL。而基本类型有默认值且不是NULL,基本数据类型直接存放在Java虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存放在堆中。相比于包装类型,基本数据类型占用的空间非常小。

为什么存在包装类型?
泛型和集合不支持基本数据类型,并且很多业务场景下,对象某些值为NULL,不需要赋值。

常见问题:
NULL指针异常情况遇到过呢?
自动拆箱引发这个问题,当数据库为NULL的数据自动拆箱赋值给基本数据类型就会出现问题。
三元运算也会出现空指针异常。因为在表达式中如果存在基本数据类型的话,那么那个包装类型就会转换为基本数据类型,就会抛出空指针异常。

超过long整型的数据应该怎么表示?
基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。利用BigInteger就可以解决。

8种基础数据类型和占内存大小

自动转换:低类型向高类型转换
强制转换:高类型向低类型转换,但可能会导致数据溢出或者精度丢失
运算符的优先级记住口令:单目乘除为关系,逻辑三目后赋值
8种基础类型

引申:Integer比较大小应该使用等于还是equals?推荐使用equals,因为-128-127范围内的Integer对象,值相同的integer对象都是指向的同一块内存空间,所以这个区间内的Integer值可以直接用==进行判断。主要是缓存的关系。long不能作为switch的判断依据,底层都是用int判断,long超过范围了。

运算优先级

注意:Java里使用long类型的数据一定要在数值后面加上L,否则将作为整型解析

红黑树的五大特征

  1. 节点要么是红色,要不是黑色
  2. 根节点是黑色
  3. 叶子节点是黑色
  4. 每个红色节点的左右孩子都是黑色
  5. 从任意节点到其每个叶子节点的所有路径,都包含了相同数目的黑色节点

搜索次数大于插入删除次数,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选用RB-Tree

二叉查找树(二叉排序,二叉搜索树),相当于二分查找,但是可能出现线性化,相等于O(n),于是出现了红黑树,它是自平衡的二叉搜索树,有红黑两个节点,根节点是红色,叶子节点是为空的黑色节点,红黑交替,从任何节点触发到达它可达的叶子节点路径所包含的黑色节点一样,增删查时间复杂度O(logn)通过变色与旋转维持平衡。左旋:逆时针旋转,让右孩子当父亲;右旋:顺时针旋转,让左孩子当父亲。

AVL:AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(logn)。
红黑树:红黑树(Red Black Tree) 是一种自平衡二叉查找树,它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(logn)时间内做查找,插入和删除,这里的是树中元素的数目。
二叉查找树:二叉查找树中查询元素的最优是O(logN)即在满二叉树的情况下,最坏时间复杂度是O(n)即除叶子节点外每个节点只有一个子节点

深拷贝和浅拷贝

浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。implements Cloneable 重写clone方法

深拷贝就是我拷贝之后对于新对象的修改不会影响原来对象,浅拷贝就是我们可以共用一个地址,双方修改对对方都会有影响。
实现方法:
● 另外一个方法就是序列化这个对象,在反序列化回来就是一个新对象,主要抓住第一句话的实质就行
● 可以重写clone()方法进行实现

解决浮点数精度丢失问题

直接上代码:

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false

利用BigDecimal:

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */

add 方法用于将两个 BigDecimal 对象相加,subtract 方法用于将两个 BigDecimal 对象相减。multiply 方法用于将两个 BigDecimal 对象相乘,divide 方法用于将两个 BigDecimal 对象相除。

Queue和Dequeue的区别

Queue是单端队列,只要从一端插入元素,另一端删除元素,实现上一般遵循先进先出(FIFO规则)
Queue扩展了Collection的接口,根据因为容量问题而导致操作失败后处理方式的不同,可以分为两类方法:一种在操作失败会抛出异常,另一种则会返回特殊值。

Deque是双端队列,在队列的两端均可以插入或删除元素。
Deque扩展了Queue的接口,增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:一种在操作失败会抛出异常,另一种则会返回特殊值。

Deque还提供了有push()和pop()等其他方法,可用于模拟栈。

Java代码的执行流程

开始 --> 父类的静态变量->父类的静态代码块 -->子类的静态变量-> 子类的静态代码块 -->父类的普通变量-> 父类的普通代码块 --> 父类的构造方法–>子类的普通变量-> 子类的普通代码块 --> 子类的构造方法 --> 结束 --------- (静态代码块只会执行一次)

SPI和API的区别

主要区别是看主动权在哪方!!!API的主动权在实现方,而SPI的主动权在调用方。

一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。

对称加密和非对称加密

● 常用的加密算法包括DES、3DES、AES、DESX、Blowfish、RC4、RC5、RC6。推荐用AES。
● 常见的非对称加密算法:RSA、DSA(数字签名用)、ECC(移动设备用)、Diffie-Hellman、El Gamal。推荐用ECC(椭圆曲线密码编码学)。
● 散列算法:MD2、MD4、MD5、HAVAL、SHA、SHA-1、HMAC、HMAC-MD5、HMAC-SHA1。推荐MD5、SHA-1。

对称加密算法需要一个key,可以加密解密,所以可以影响安全性,解决方法可以用加盐hash来解决

谈谈面向对象的理解

**面向过程:**一件事情该怎么去做,注重实现过程,以过程为中心。
**面向对象:**实现对象是谁,只关心怎么使用,不关心具体实现(只关心实现对象是谁,有封装、继承、多态三大特征)

面向对象三大特征:
● 封装:可以不关心内部实现,具体构造,只需知道怎么操作它就是,比如电视,手机,将内部封装起来,直接使用。
● 多态:同一个方法调用,由于对象不同可能会有不同的行为。比如都是休息,张三是睡觉,李四是爬山等;或者具体场景中,我不知道现在具体传过来的对象是student还是teacher,那么可以用people去接收它。多态的存在要有三个必要条件:继承、方法重写,父类指向子类对象(父类指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了,但是要记住编译看父类,运行才看子类)
● 继承:是代码更容易扩展,比如有学生教师,他们都有一些公有方法和属性,可以将其抽取出来定义为父类,再去继承它,复用代码,减少冗余,易于扩展。

面向对象是一种编程思想,早期的面向过程的思想就是一件事怎么做,而面向对象就是一件事该由谁来做,它怎么做的我不管,我只需要调用就行。而这些是由面向对象的三大特性来实现的,三大特征就是封装、继承、多态。封装就是将一个类的属性和行为抽象成一个类,使其属性私有化,行为公有化,提高属性的安全性的同时,也可以使得代码模块化,这样做就使代码的复用性更高。继承就是将几个类的公有的属性和行为抽象成一个父类,每个子类都有父类的属性和行为,也有自己的属性和行为,这样做,扩展了已存在的代码,进一步提高的代码的复用性,但是继承是耦合度很高的一种关系,父类代码修改,子类行为也会改变,如果过度使用继承会起到反效果。多态必须要有继承和重写,并且父类/接口引用指向子类/实现类对象,很多的设计模型都是基于面向对象中多态性的设计的。

组合是has-a的关系,组合就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。优先使用对象组合是面向对象设计原则的第二原则。
优先使用组合,继承还是相对于耦合度很高的一种关系

面向对象设计原则
● 开闭原则:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭(接口和抽象类)->易于扩展,维护升级
● 单一职责原则:类和方法实现单一的职责,专注于一件事。(类只做一件事)
● 里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能,可以新增方法,实现抽象方法。(继承)
● 依赖倒置原则:面向接口编程,细节依赖于抽象。
● 接口隔离原则:接口职责单一。
● 迪米特法则:两个软件实体无需直接通信,引入第三方转发调用,间接通信。只和中间者进行相应的沟通
● 合成复用原则:尽量使用对象组合/聚合实现软件复用,其次才是继承关系(将已有对象组合到对象中,复用

面向对象和面向过程的区别

面向过程:面向过程性能比面向对象高。因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,但是,面向过程没有面向对象易维护、易复用、易扩展。
面向对象:面向对象易维护、易复用、易维护。因为面向对象有封装性、继承、多态性的特征,所以可以设计出低耦合的系统,使系统更加灵活,更加易于维护。但是,面向对象性能比面向过程低。

JDK与JRE的区别

  1. JDK(java开发工具):JDK=JRE+开发工具集(java javac javadoc jar…)
  2. JRE(java运行环境):JRE=JVM+核心类
    ● 什么是字节码:在Java中,JVM可以理解的代码就叫做字节码(.class文件),它不面向任何特定的处理器,只面向虚拟机。Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无需重新编译便可在多种不同操作系统的计算机上运行。
    ● Java是编译和解释共存的语言:Java引入了JIT编译器(即时编译器),而JIT编译器属于运行时编译。当JIT编译器完成第一次编译后,其会将字节码对象的机器码保存下来,下次可以直接使用。性能有所提升。JDK9引入AOT,直接把字节码编译成机器码,减少预热开销,但是性能比不上JIT。
    ● 注意:JSP部署Web应用程序,需要JDK,因为JSP转换为Java Servlet,需要进行编译。
    ● 只运行,JRE即可,要编译就要JDK

为什么不全部使用AOT呢?
AOT可以提前编译节省启动时间,那为什么不全部使用这个编译方式呢?
动态代理->CGLIB动态代理使用的是ASM技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码也就是.class文件,如果全部使用AOT提前编译,也就是不能使用ASM技术了。为了支持类似的动态特性,所以选择使用JIT即时编译器。

ASM技术:ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

值传递与引用传递

Java编程语言只有值传递参数。(都是传递副本)
● 如果参数类型是基本数据类型,不会改变原始的值,传递的参数的副本。
● 如果参数类型是引用数据类型,传递的是引用参数的副本,存放的是参数的地址值,同样会创建副本。扩展深拷贝,浅拷贝。
引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响实参。

区别:Java中都是值传递

==与equals()区别

==比较的是地址,对于基本数据类型比较的是值,对于引用数据类型比较的是内存地址是否相同,equals()属于Object类方法,没有重写之前使用效果和等于一样。可以重写例如String类,使其比较内容值是否相等。

为什么重写equals时必须重写hashcode方法

hashcode可以获得哈希码,是对象在内存中的地址转换成的int值,确定该对象在哈希表中的索引位置
● 提高效率:使用hashcode提前检验,定位,不用每一次都是要equals()方法比较,提高效率
● 保证没有重复对象出现,确保hashmap去重性:假如只重写equals方法,不重写hashcode,相同的对象hashcode不同,从而映射不同下标下,hashmap无法保证去重
euqals相同,hashcode相同,hashcode不同,equals一定不同,hashcode相同,equals不一定相同(hash冲突)
如何解决hash冲突:链地址法、开放地址(外加增量)【线性探测再散列、二次探测再散列】、再哈希(hash函数不同)、建立公共溢出区

创建对象在内存中做了哪些事情

实例:Student stu=new Student()

  1. 载入Student.class文件进入内存(方法区)
  2. 在栈内存为stu开辟空间
  3. 在堆内存为学生对象开辟空间
  4. 对学生对象的成员变量进行默认初始化
  5. 对学生对象的成员变量进行显示初始化
  6. 通过构造方法对学生对象的成员变量进行赋值
  7. 学生对象初始化完成,把对象地址赋值给stu变量。

显示初始化是针对成员变量本身有赋值情况。

重载和重写的区别

方法的重载和重写都是实现多态的方式,区别在与前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载(静态绑定):发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型区分。
重写(动态绑定):发生在父子类中,方法名、参数列表必须相同,返回值小于父类,抛出的异常小于父类,方法修饰符大于父类(里氏交换原则);如果父类方法访问修饰符为private/final/static则子类中就不是重写,但是static修饰方法能够被再次声明。【方法修饰符:public>protected>default>private】
里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能,可以新增方法,实现抽象方法。(继承)。

String有哪种编码格式

ASCII GBK UTF-8
举例:

String s = "Welcome to smile"; 
byte[] b = s.getBytes("UTF-8"); 
String n = new String(b,"UTF-8"); 

String,StringBuffer和StringBuilder之间的区别是什么

从下面三点进行回答:
● 可变性:String是不可变对象,任何对String修改都会创建新的String对象,Stringbuffer和Stringbuilder可变类。
● 效率:频繁对字符进行操作时,使用String会生成一些临时对象,多一些附加操作,效率低些。
● 安全性:Stringbuffer方法由synchronized修饰,线程安全。
可以将知识衍生到synchronized重量级锁和锁的优化
String是final修饰的类,是不可变的,所以是线程安全的。

关于String的理解

这里主要就是string中涉及到字符串常量池,string对象创建要好好区分,主要区别其中地址是否发生变化。
补充:intern()是获取常量池的string对象

//相关示例代码
String str1 = new String("123");//在常量池创一个“123”对象,遇到new在堆内存创建一个对象,并返回堆中的对象引用
String str2 = "123";//因为之前常量池中能找到“123”的对象,所以直接将引用返回,不创建新的对象
String str3 = str1.intern();//若常量池中包含了str1字符串“123”,则直接返回引用,否则就在池中先创建一个在返回池中的对象引用
System.out.println((str1 == str2) +","+ (str3 == str2))
output:false,ture
String str4 = new String("234");
String str5 = new String("234");
String str6 = str4.intern();
String str7 = str5.intern();
System.out.println((str4 == str5) +","+ (str6 == str7));
output:false,ture

为什么用final修饰String类?
● 为了实现字符串常量池。因为只有当字符串是不可变的,字符串池才有可能实现。
● 为了线程安全
● 为了实现String可以创建Hashcode不可变性。因为字符串是不可变的,所以在它创建的时候Hashcode就被缓存了,不需要重新计算。
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。

使用过可变长参数吗

从Java5开始,Java支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数,就比如下面的这个,method方法可以接收0个或者多个参数

public static void method(String... args) {
   //......
}

遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?

会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。

Java集合机制与使用场景

  1. Collection
    一组"对立"的元素,通常这些元素都服从某种规则
      1.1) List必须保持元素特定的顺序 (有序、可重复、查找效率高、插入删除低、下标遍历)
    1.1.1) ArrayList
    1.1.2) Vector
    1.1.3) LinkedList
      1.2) Set不能有重复元素(无序、不可重复、查询效率低)
    1.2.1) HashSet(为快速查找设计,)
    1.2.1.1) LinkedHashSet
    1.2.2) SortSet
    1.2.2.1) TreeSet(使得有序)
      1.3) Queue保持一个队列(先进先出)的顺序
    1.3.1) PriorityQueue(模拟堆,按照元素顺序排序)
    1.3.2) Deque
  2. Map
    一组成对的"键值对"对象
    2.1) HashMap
    2.2) HashSet
    2.3) SortedMap
    2.3.1) TreeMap(基于红黑树排序 O(logn))

Set与List区别

  1. List,Set都是继承自Collection接口
  2. List特点:元素有放入顺序,元素可重复,Set特点:元素无序,元素不可重复,重复元素会覆盖掉,(元素虽然无放回顺序,但是元素在set中的位置是有该元素的HashCode决定,其位置其实是固定的,加入Set的Object必须定义equals()方法,另外List支持for循环,也就是通过下标遍历,也可以迭代器,但是Set只能用迭代器,因为他无序,无法用下标来去的想要的值。
  3. Set和List对比:
    ● Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    ● List:和数组类似 ,List可以动态增长,查询元素效率高,插入和删除元素效率低,因为会引起其他元素位置改变,底层数组实现

ArrayList扩容机制

● 添加元素时使用ensureCapacityInternal()方法来保证容量足够,如果不够时,需要使用grow()方法进行扩容
● 新容量大小为oldCapacity+(oldCapacity>>1)即oldCapacity+oldcapacity/2,其中oldCapacity>>1需要取整,所以新容量大约是旧容量的1.5倍左右。(oldCapacity为偶数就是1.5倍,而奇数就是1.5倍-0.5)
● 扩容操作需要调用Arrays.copyof()把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList对象时就指定大概的容量大小,较少扩容操作的次数。
● modCount记录结构修改次数

ArrayList与Vector的区别

● 实现:都实现List接口,底层采用Obect[] elementData数组
● 线程安全:Vector使用了synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的
● 性能:ArrayList在性能方面优于Vector
● 扩容:ArrayList和Vector都会根据实际的需要动态地调整容量,只不过在Vector扩容每次是2倍,而ArrayList变1.5倍
● 长度:ArrayList和Vector默认初始长度10
● ArrayList实现线程安全的方式可以采用这个方式Collections.synchronizedList(new ArrayList())

ArrayList和LinkedList的区别

● ArrayList与LinkedList都实现List接口:ArrayList实现了RandomAccess接口,代表支持随机访问,所以查询效率就要高些。
● 线程安全问题:ArrayList与LinkedList都是线程不同步的,也就是都不保证线程安全。
● 底层数据结构:ArrayList底层使用数组,默认初始大小为10,插入元素超过阈值则会动态扩容为原来1.5倍;LinkedList底层采用双向链表数据结构(注意:JDK1.6之前为循环链表,JDK1.7取消了循环)
● 插入删除:ArrayList:若增加至末尾,O(1);若在指定位置i插入O(n-i)。LinkedList:插入删除都是近似O(1),而数组为近似O(n)。
● 查询:数组支持随机快速访问,而链表需要依次遍历,更耗时
● 占用内存空间大小:一般LinkedList占空间更大,双向链表每个结点都维护两个指针。但是若ArrayList刚到扩容阈值,扩容后会浪费很多空间。
● 数组查找原理:数组空间连续,查询通过偏移量找,LinkedList底层链表,逻辑连续,空间不连续,指针访问数据。

HashMap和Hashtable的区别

● 线程安全:Hashtable方法Synchronized修饰,修饰安全
● 效率方面:由于Hashtable方法被Synchronized修饰,效率比Hashmap低
● 底层数据结构:HashMap jdk1.8当链表长度>=8并且数组长度>=64链表会转为红黑树,hashtable没有这样的机制

为什么阈值是8呢?
hash碰撞发生8次的概率已经低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因。

为什么是64?低于64,hash碰撞的几率比较大,这种时候出现长链表的可能性比较大,这种原因导致的长链表我们应该避免,而是采用扩容的策略避免不必要的树化
● 初始容量与扩容:默认初始量:Hashtable为11,HashMap为16;若指定初始量:Hashtable用指定的值,HashMap会扩充为2的幂次方大小。扩容:Hashtable容量变为2n+1倍,HashMap变为2倍

● 对Null key与Null value支持:HashMap支持一个Null key 多个 Null value,Hashtable不支持Null key,会报错空指针异常
● 为什么hashMap支持Null key以及Null value的原因是hashMap中使用hash()方法来计算key的hash值,当key为空时,直接另key的哈希值为0,不走key.HashCode()方法。
● hashtable,对于null键,则会调用null.hashCode(),而导致空指针异常,而concurrenthashmap则对于null键值对,直接抛出空异常
● ConcurrentHashmap和Hashtable都是线程安全用来做支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。

HashSet怎么检查重复

对HashMap封装了一层,很多方法还是直接用的map的,通过计算对象的hashcode定位,同时比较与其他对象hashcode是否相同,若没有相同的则假设没有重复对象;若有相同的则equals判断。

HashMap与HashSet区别

HashSet底层是基于Hashmap实现的(除个别方法自己实现,其他调用hashmap的)。hashmap使用键(key)计算hashcode,hashset使用成员对象来计算hashcode。

HashMap jdk8与jdk7区别

● jdk8中新增了红黑树,jdk8是通过数组+链表+红黑树(logn)来实现的
● jdk7中链表的插入是用的头插法,而jdk8中则改为尾插法(因为jdk7是用单链表进行的纵向扩展,当采用头插法时会容易出现逆序且链表死循环的问题)
● jdk8因为使用了红黑树保证了插入和查询的效率,所以实际上jdk8中的hash算法实现的复杂度降低了
● jdk7中是先扩容再添加新元素,jdk8中是先添加新元素然后再扩容
● jdk8中数组扩容的条件也发生了变化,只会判断是否当前元素个数是否超过了阈值,而不再判断当前put进来的元素对应数组下标位置是否有值。

HashMap和TreeMap区别

TreeMap和HashMap都继承自AbstractMap,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap接口。
实现NavigableMap接口让TreeMap有了对集合内元素的搜索能力
实现SortedMap接口让TreeMap有了对集合中的元素根据键排序的能力。默认是按key的升序排序,不过我们也可以指定排序的比较器。
综上,相比于HashMap来说,TreeMap主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索能力。

HashMap的长度为什么是2的幂次方

1&1=1 1&0=0 1^1=0 1^0=1 1|1=2

因为当容量为2的幂次方时,h&(length-1)运算才等价于对length取模,也就是h%length,而&比%具有更高的效率,也就是计算机会计算的更快。
并且可以尽量保证均匀分布减少hash冲突。
2的n次方实际就是1后面有n个0,2的n次方-1就是有多少个1.这样按位“与”运算时,每一位都能进行与运算,正在参与了运算,分布更加的均匀。

为什么要把key的哈希码右移16位呢

  1. 经过hash函数计算得到hash值(先计算key的hashcode,在计算h^(h>>>16)) 目的就是较少hash冲突
  2. 通过(n-1)&hash判断当前元素存放的位置

HashMap的put方法流程

  1. HashMap通过hash函数处理之后得到hash值,然后(n-1)&hash计算落位下标(n指的是数组的长度),如果当前下标位置存在元素的话,就判断元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同的话就要拉链法解决冲突。
  2. 执行put方法会执行putval方法,执行putval前先计算hash值
  3. 经过扰动函数使其hash值更加散列(调用key对象的hashcode方法计算出来hash值,将hash值得高16位与原hash值取异或运算(^),混合高16位和低16位的值,得到一个更加散列的低16位的hash值)
  4. 然后进入putval方法,会判断是否第一次调用put,若是第一次才初始化数组长度为16
  5. 然后会判断数组该位置是否为空,若空创建结点插入,不为空若插入元素与捅中元素key一样,后面替换。若不为空且插入元素与捅中元素key不一样,则向后添加元素(jdk7,头插法,jdk8尾插法,遍历链表,若有相同的node就替换,否则尾插,然后再判断是否树化(链表长度大于或等于8,进入treeifyBin(tab,hash);进入该方法还需要判断当前数组长度>=64,才能树化,如果<64则扩容),树化的过程中会进行相应的节点转换。

Hashmap并发线程安全问题

  1. 在jdk1.7中,当并发执行扩容操作会造成环形链,然后调用get方法会出现死循环【多线程put操作后,get操作导致死循环,导致cpu100%的现象。主要是多线程同时put时,如果同时触发了rehash操作,会导致扩容后的hashmap中的链表中出现循环节点,进而使用后面get的时候,会死循环】
  2. 在jdk1.8中,并发执行put操作时会出现数据覆盖的操作 ABA
  3. 在jdk1.8时,也会出现死循环,比如:
    a. 链表转换为树
    b. 对树进行操作时
final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;   //1816行
            }
        }

hashmap扩容流程

扩容时多线程操作可能会导致链表成环的出现,然后调用get方法会死循环
触发时机:1.未初始化,第一次put时
2.大于扩容阈值(加载因子0.75)
流程:新建2倍大小的数组,根据新数组长度对其值重新hash,寻址。
jdk1.7下的扩容机制:

    resize()方法
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    transfer()方法
        /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;              //注释1
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity); //注释2
                e.next = newTable[i];                  //注释3
                newTable[i] = e;                       //注释4
                e = next;                              //注释5
            }
        }
    }

jdk1.7采用的是头插法
补充:jdk1.8中扩容优化:不需要像jdk1.7的实现重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好,是0的话索引不变,是1的话索引变成"原索引+oldCap"

代码提示: e.hash&oldCap

TreeMap&TreeSet

TreeMap是基于红黑树的一种提供顺序访问的map,复杂度都是O(logn);默认按键的升序排序,具体顺序可有指定的comparator决定。TreeMap键、值都不能为Null,hashtable也不能为Null。

TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。TreeSet 用于支持对元素自定义排序规则的场景

反射原理(属性、构造器、注解、方法等)

● 什么是反射:动态地获取类的各个属性以及调用它的方法(Spring中IOC实现就用到了反射)
● 原理:通过将类对应的字节码文件加载到jvm内存中得到一个class对象,通过这个class对象可以反向获取实例的各个属性以及调用它的方法。
● 缺点:性能瓶颈/安全问题
● 获取class对象的方式:
a. Object->getclass();(对象.getclass())
b. 任何数据结构(包括基本数据类型)都有一个“静态”的class属性(类.class)
c. Class.forName(“”)
d. loadClass()
● 使用场景:
a. 通过反射运行配置文件内容
ⅰ. 加载配置文件,并解析配置文件得到相应信息
ⅱ. 根据解析的字符串利用反射机制获取某个类的class实例对象
ⅲ. 动态配置属性
b. JDK动态代理
c. jdbc通过Class.forName()加载数据的驱动程序
d. Spring解析xml装配Bean

补充:Class.forName和Classloader的区别?
forName会初始化Class,而LoadClass不会初始化,因此如果要求加载类的静态变量和静态代码块,就需要forName,而LoadClass只能等创建类的实例的时候才能初始化。newInstance()->类的实例化
反射使用相关的api:https://www.jianshu.com/p/d8b3495f52a7
包装类:Class< Integer > type = Integer.TYPE;

Object的方法有哪些

  1. getclass:final方法,获取运行时状态
  2. toString():对象的字符串表示形式(对象所属类的名称+@+转换为十六进制对象的哈希值组成的字符串)
  3. equals方法:如果没有重写用的就是Object里的方法,和==一样就是比较两个引用地址是否相等,或者基本数据类型值是否相等
  4. Clone方法:实现对象的浅拷贝,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常
  5. notify方法:环形在该对象上等待的某个线程
  6. notifyAll方法:唤醒在该对象上等待的所有线程
  7. wait方法:作用是让当前线程进入等待状态,同时,wait方法也会让当前线程是方法它所持有的锁,直到其他线程调用此对象的notify和notifyAll方法,当前线程被唤醒(进入就绪状态)。还有一个wait(long timeout)超过时间,sleep休眠不会是方法锁
  8. Finalize方法:可以用于对象的自我拯救。
    引申:为什么不能显示直接调用Fanalize方法?
    Finalize方法在垃圾回收时一定会被执行,而如果在此之前显示执行的话,也就是说Finalize会被执行两次以上,而在第一次资源已经被释放,那么在第二次释放资源时系统一定会报错。一般Finalize方法的访问权限和父类保持一致,为protected,因为Finalize方法时Object方法。
  9. Hashcode方法:该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法,必须重写hashcode方法,这个方法在一些具有哈希功能的Collection中用到。equals相等,hashcode一定相等,hashcode相等,equals不一定相等。hash实现的方式还有平方取中法、随机数法、折叠法等。

Java接口和抽象类的区别

● 方法:接口只有定义,不能有方法的实现,jdk8中可以定义default与static方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
● 成员变量:接口成员变量只能是public static final的,且必须初始化,抽象类可以和普通类一样。
● 继承实现:一个类只能继承一个抽象类(extends),可以实现多个接口(implements)
● 都不能实例化
● 接口不能有构造函数,抽象类可以有
设计层面:
● 类是“是不是”关系,接口是“有没有”关系
● 抽象类作为很多子类的父类,他是一种模板式设计。而接口时一种行为规范,它是一种辐射式设计。
● jdk1.8以后可以有方法体,用default修饰即可:

public interface IDefault{
    //让default修饰
    default int testB(){
        return 123;
    }
}
//抽象类
public abstract class Area{ 
    abstract void area();//实现类去实现这个类
}

既然有了字节流,为什么还要有字符流

Java虚拟机转字节得到字符流耗时;不知道编码格式很容易出现乱码,所以才引进了字符流。

throw与throws的区别

throw是用在了方法内部,只能用于抛出一个异常;throws关键字在方法声明上,可以抛出多个异常,用来标记该方法可能抛出的异常列表。

动态代理

  1. 动态代理的好处:
    一个工程如果依赖另一个工程给的接口,但是另一个功能的接口不稳定,经常变更协议,就可以使用代理,接口变更时,只需要修改代理,不需要修改业务代码。
  2. 作用:
    ○ 功能增强:在原有功能加新功能
    ○ 控制访问:代理类不让你访问目标
  3. JDK动态代理:利用反射机制生成代理类,可以动态指定代理类的目标类,要求实现invocationHandler接口,重写invoke方法进行功能增强,还要求目标类必须实现接口。
  4. cjlib动态代理::把动态代理的class文件加载进来,修改其字节码文件生成子类,子类重写目标类的方法,被final修饰的不可以,然后在子类采用方法拦截技术拦截父类方法调用,织入逻辑(定义拦截器实现MethodInterceptor接口),但是有个弊端就是子类继承不了父类final、private方法。

异常体系

Throwable的子类为Error和Exception
异常分为运行时异常和编译时异常
Error就是一些程序处理不了的错误,代表JVM出现了一些错误,应用程序无法处理。例如当JVM不在有继续执行操作所需的内存资源时,将出现OOM
常见异常:空指针异常,ClassNotFoundException,数组下标越界,类型强制转换。
ClassCastException是JVM在检测到两个类型间转换不兼容时引起的运行时异常,是运行时异常。
异常体系

BIO.NIO,AIO有什么区别

补充知识:
同步和异步的区别在于异步的被调用这会通过回调等机制来获得调用者的返回结果。
堵塞和非堵塞的区别在于请求发起之后,是否等待结果返回之前啥事都不做。

● BIO:同步堵塞IO模式,数据读取写入必须堵塞在一个线程内等待其完成。
● NIO: 同步非堵塞的IO模型,引申出IO多路复用技术(select、poll、epoll)
● AIO:异步非堵塞IO模式,AIO就是NIO2,在JDK7中引入了NIO的改进版NIO2,他就是异步非堵塞的IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里。
根据冯.洛依曼结果,计算机结构分为5大部分:运算器、控制器、存储器、输入设备、输出设备

泛型

Java泛型(generics)是JDK5中引入的新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员正在编译时检测非法的类型。泛型的本质是参数化类型,也就是说锁操作的数据类型被指定为一个参数。
泛型擦除:所有泛型在编译后都会被擦除。
● extends和super的区别?
泛型中extends的主要作用是设定类型通配符的上限
泛型中super的主要作用是设定类型统配的下限
● 泛型中类型擦除转换成Object就不能进行大小比较了
● static不能修饰泛型
● 这些都是由于泛型存在类型擦除问题

final关键字

不能被重写、改变、继承,并且String是由final修饰。
final修饰变量的三种赋值方式
● 在定义时直接复制
● 声明时不赋值,在constructor中赋值(最常用的方式)
● 声明时不赋值,在构造代码块中赋值

static关键字

● 一般方法可以访问静态方法,静态方法不能访问非静态方法。
● 修饰变量是,类加载进方法区,所以多个对象共享变量,引申类加载过程
● this()和super()都指的是对象,均不可以在static环境中使用
● super()和this()均需放在构造方法内第一行
● this和super不能同时出现在一个构造函数里面,因为this必然会调用其他的构造函数,其他的构造函数必然也会有super语句的存在,所以在同一个构造函数有相同的语句就会失去了语句的意义,编译器也不会通过。

类加载机制:
加载:根据类的全限定类名获取二进制字节流,将字节流代表的静态存储结果转为运行时存储结果,在内存生成class对象,作为方法区这个类数据访问入口。
验证:检验加载的class文件正确性
准备:为类的静态变量分配内存,并赋默认值
解析:将常量池中符号引用转为直接引用(class文件常量池转为方法区运行时常量池)
初始化:将静态变量和静态代码块执行初始化工作。

父类的静态属性方法是否可以被子类继承和重写?
可以继承,但是只能“隐藏”式的传递给子类,子类还是拥有父类的静态属性方法,调用的时候还是调用父类的静态属性和方法。但是不能被重写

序列化与反序列化

序列化:将对象写入到IO流中;反序列化:从IO流中恢复对象
使用场景:所有可在网络中传输的对象都必须是可序列化的(引申:RPC机制)
如果你不想被序列化,则加入transient关键字即可。
同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
建议所有可序列化的类加上serialVersionUID版本号,方便项目升级(如果反序列化使用的class版本号与序列化使用的不一致,反序列化回报InvalidClassException异常)

RPC机制

报文(message)是网络中交换与传输的数据单元,即站点一次性要发送的数据块。
RPC基本流程:
1、首先调用方需要有个RPC_client,被调用方需要有 RCPServer,这两个服务用于RPC通信。
2、调用者通过RPC_Client调用指定方法,RPCClient则将请求封装后(将方法参数值进行二进制序列化),传递给server
3、server收到请求后,反序列化参数,恢复成请求原本的形式,然后找到对应的方法进行本地调用。将方法的返回值通过RPC返回给client
4、client收到结果后,将结果返回给调用者作为返回值。

访问修饰符

public protected private default

内部类

内部类包括:成员内部类、静态内部类、匿名内部类、局部内部类
匿名内部类的优点可以联系着创建Comparator对象,重新compare方法
静态内部类不依赖于外部类实例而被实例化,而非静态内部类需要在外部类实例化后才可以被实例化。还需要考虑静态与非静态的区别。

静态变量和成员变量的区别

1.所属不同:
静态变量属于类,所以称为类变量
成员变量属于对象,所以称为对象变量
2.内存中位置不同:
静态变量储存于方法区的静态区
成员变量储存于堆内存中
3.内存出现世界不同:
静态变量随着类的加载而加载,随着类的消失而消失
成员变量随着对象的创建而存在,随着对象的消失而消失
4.调用不同:
静态变量可以通过类名调用,也可以通过对象调用
成员变量只能通过对象名调用

一个类中的方法名是否可以和类名相同

可以,构造方法和类名相同。普通方法也可以和类名相同,它和构造方法唯一的区别就在于有返回类型。

Java中"~"运算符的含义

记住:(~x) = -(x + 1)
补充-5的二进制表示是
负数:
0000 0101
反码:1111 1010
补码:1111 1011
正数的补码就是原码本身
case:计算机是以补码的形式来存储的

ArrayList和LinkedList能否被序列化

ArrayList只序列化已经保存的数据,而transient是不被序列化的。LinkedList是支持序列化的。因为他们都实现了java.io.Serializable接口

System.exit(0) system.exit(1)含义和区别

前者正常退出,后者非正常退出
try…catch…finally
finally块中的代码不一定执行,比如,try块执行之前,出现了异常,则程序终止,比如,在try块中执行了System.exit(0)

在处理异常的时候,需要注意:捕获异常的时候,先获取子类异常,在获取父类异常。

希望大家有关于求职路上的那些事有哪些好的建议,可以评论区留言!如有技术探讨可以私聊!

后续会陆续更新,敬请期待!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值