赞
踩
1. Java与C++的区别:
2. 标识符与关键字:
标识符就是一个名字,关键字是Java中具有特殊含义的标识符。
Java中的关键字:
3. 自增自减运算符:
4. 移位运算符:
5. continue、break、return的区别:
6. 包装类是在基本类型的基础上,继承了Object类,并提供了一些额外的功能:Byte、Short、Integer、Long、Double、Float、Boolean、Character。
7. 包装类与基本类型的区别:
8. 成员变量与局部变量的区别:
9. 静态变量的作用:
静态变量也就是被static关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。
10. 静态方法与实例方法的区别:
11. 重载与重写的区别:
12. 抽象类和接口的共同点和区别:
共同点:
区别:
13. 深拷贝、浅拷贝以及引用拷贝:
14. Object类的常见方法:
getClass、hashCode、equals、clone、toString、notify、notifyAll、wait、wait(timeout)、wait(timeout, nanos)、finalize
15. == 和 equals()的区别:
==
因为 Java 只有值传递,所以对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
equals() 方法存在两种使用情况:
String 中的equals方法是被重写过的,因为Object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
16. hashcode()有什么用?
hashCode() 的作用是获取哈希码(int 整数),这个哈希码可以确定对象在哈希表中的索引位置。Java 中的任何类都包含有 hashCode() 函数,因为定义在Object类中。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速检索出对应的“值”,这其中就利用到了散列码(可以快速找到所需的对象)。
17. 为什么需要hashCode?
以“HashSet 如何检查重复”为例来说明。
当把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值比较,如果没有相同的 hashCode,HashSet 会假设对象没有重复出现。如果有相同 hashCode 值的对象,会调用equals()方法检查 hashCode 相等的对象是否真的相同。如果相同,HashSet 就拒绝加入。如果不同,就会重新散列到其他位置。这样就大大减少了 equals 的次数,也就提高了执行速度。
【其实hashCode() 和 equals()都是用于比较两个对象是否相等】
(1)为什么不只提供 hashCode() 方法呢?
因为两个对象的 hashCode 值相等并不代表两个对象就相等。
(2)为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为可能会存在哈希冲突,不同的对象可能拥有相同的hashcode。
(3)总结
(4)为什么equals()方法被重写时,hashCode()也要被重写?
因为两个相等对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断两个对象是相等的,hashCode 值却不相等。
(5)equals判断相等,hashcode也相等吗?分情况,equals没重写,默认就是判断hashcode是否相等;如果重写了,hashcode也要重写。Java规定,两个对象相等,hashcode也要相等
(6)思考 :重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题?会出现像(4)中提到的那样,可能会导致equals方法判断两个对象相等,但hasdCode值却不相等,并且由于hashCode()的不一致性,可能导致相等的两个对象被放置在不同的桶中,使得无法正确地获取等效的对象,从而导致无法正常进行元素的查找、更新或删除操作。
18. Exception和Error的区别:
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的Throwable类。Throwable类有两个重要的子类Exception和Error:
19. Throwable类常用的方法:
20. 什么是泛型?
泛型,即参数化类型。编译器可以对泛型参数进行检测,并通过泛型参数可以指定传入的对象类型,比如说,原生 List 返回类型是 Object,需要手动转换类型才能使用,使用泛型后编译器自动转换。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
21. 什么是反射
反射可以在运行时动态地操作类和对象,并且能够适应不确定的类型;但需要维护代码的可读性和性能。
业务代码中涉及反射较少,但我们常用的各类框架,如Spring、Spring Boot、MyBatis等都使用了大量的反射机制。
22. SPI和API
SPI是Service Provider Interface,服务提供接口,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
23. Java值传递
- String hello = "Hello!";
- // hello 为实参
- sayHello(hello);
- // str 为形参
- void sayHello(String str) {
- System.out.println(str);
- }
程序设计语言将实参传递给方法(或函数)的方式分为两种:
Java中只有值传递。
24. Java序列化
如果需要持久化 Java 对象,比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
应用场景:
序列化通常对应于TCP/IP 4层模型的应用层(表示层)。
25. 动态代理方式:JDK动态代理与CGLIB动态代理
代理模式有静态代理和动态代理两种实现方式。动态代理的实现方式有很多,比如 JDK 动态代理、CGLIB 动态代理等
RPC使用了JDK动态代理。
(1)JDK动态代理
在Java动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。
使用步骤:
(2)cglib动态代理
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,可以用 CGLIB 动态代理机制来避免。
cglib(Code Generation Library)字节码生成库,允许在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理,很多框架使用到了cglib。比如Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
使用步骤:
(3)两者对比
26. BigDecimal
为什么浮点数float或double运算的时候会有精度丢失的风险呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
- float a = 2.0f - 1.9f;
- float b = 1.8f - 1.7f;
- System.out.println(a);// 0.100000024
- System.out.println(b);// 0.099999905
- 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.compareTo(y));// 0
27. Java语法糖
指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。
Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。
常见的语法糖:
()
和hashCode()
方法来实现的)28(补充).JVM、JRE、JDK分别是什么,有什么区别?
JVM,Java虚拟机,是Java程序运行的环境,主要作用是将Java代码转换为可以在计算机上运行的机器码,并负责程序的执行。
JDK是Java开发工具包,包含了编写、编译、调试和运行Java程序时所需要的全部工具和组件,包含JRE。
JRE是Java运行时环境,包括JVM和Java API,提供了在计算机上运行Java程序所需的最小环境。
1. Java中有哪些集合?哪些是线程安全的?
Java集合又称为容器,主要由两大接口派生而来:一个是Collection接口,主要用于存放单一元素;另一个是Map接口,主要用于存放键值对。Collection接口下又有3个主要的子接口:Set、List、Queue。
Java集合中,线程安全的有:Vector、Hashtable、ConcurrentHashMap。
线程不安全的有:ArrayList、LinkedList、HashMap、HashSet。
2. List、Set、Map、Queue四者的区别:
3. 集合框架底层的数据结构
(1)List
(2)Set
(3)Queue
(4)Map
4. 如何选用集合?
我们主要根据集合的特点来选择合适的集合。比如:
5. ArrayList和Array的区别:
ArrayList 内部基于动态数组实现,比Array(静态数组) 使用起来更加灵活:
6. ArrayList插入删除元素的时间复杂度
7. LinkedList插入删除元素的时间复杂度
8. LinkedList无RandomAccess接口。
RandomAccess是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess接口。
9. ArrayList与LinkedList的区别:
(1)是否保证线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
(2)底层数据结构:ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。)
(3)插入和删除是否受元素位置的影响:
(4)是否支持快速随机访问:LinkedList 不支持随机元素访问,而 ArrayList(实现了RandomAccess接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
(5)内存空间占用:ArrayList 的空间浪费主要体现在 list 列表的结尾会预留一定的容量空间;LinkedList 的空间花费体现在它的每一个元素都需要存放直接后继和直接前驱以及数据。
10. HashMap与Hashtable的区别:
(1)线程是否安全:HashMap 不是线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果要保证线程安全的话就使用 ConcurrentHashMap );
(2)效率:因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。Hashtable 基本被淘汰;
(3)对 Null key 和 Null value 的支持:HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
(4)初始容量大小和每次扩充容量大小的不同:
① 创建时如果不指定容量初始值,Hashtable 默认初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16,之后每次扩充,容量变为原来的2倍。
② 创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,HashMap会将其扩充为2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。
(5)底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable 没有这样的机制。
11. HashMap与HashSet的区别:
HashSet的底层实现是基于HashMap,并且其源码非常少,除了clone()、writeObject()、readObject()等不得不实现的方法之外,其余的方法几乎都是直接调用的HashMap。
12. HashMap与TreeMap的区别:
相比于HashMap来说,TreeMap主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
13. HashMap的底层实现
JDK1.8之前:
HashMap 底层是数组和链表一起使用也就是链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置( n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的是 HashMap 的 hash 方法,使用扰动函数之后可以减少碰撞。“拉链法”将链表和数组结合,创建一个链表数组,数组中每一格是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后:
JDK1.8之后在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为8)时,会将链表转换为红黑树,以减少搜索时间。在转换为红黑树之前会进行判断,如果数组长度小于64,会先对数组进行扩容。
红黑树是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
14. HashMap的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是”(n-1)&hash“。
算法如何设计?
⾸先可能会想到采用%取余操作来实现。但“取余操作中,如果除数是2的幂次则等价于和其除数减⼀的与操作【也就是说 hash%length == hash&(length-1)的前提是 length 是2的 n 次方】”并且采⽤⼆进制与操作,相对于%能够提⾼运算效率,这也就解释了 HashMap 的长度为什么是2的幂次方。
举个例子:
假设数组的长度是16,那么长度减一就是15。用二进制表示如下:
现在假设我们有一个哈希值 hash=22,用二进制表示是10110。我们想要计算这个哈希值对应的数组索引。
位与操作是通过二进制计算,22 -> 10110;15 -> 01111,两个数的每一位进行比较,如果两个数在某一位上都是1,则该位的结果为1,否则为0。 结果为00110(6)。
可以看到,在这个例子中,hash%length 的结果和 hash&(length-1)的结果是相同的。
15. HashMap多线程操作导致死循环问题
HashMap在扩容过程中,如果有多个线程同时进行这个操作,可能会导致链表中的元素形成环形结构,从而导致死循环。
具体来说,当一个线程正在对HashMap进行扩容操作(rehash)时,它会遍历旧数组中的所有元素,并将它们重新放置到新数组中的正确位置。如果在这个过程中,另一个线程也尝试插入或删除元素,它可能会破坏正在进行的rehash操作的链表结构,导致某些元素在链表中形成环形结构。当后续的操作尝试遍历这个链表时,就会陷入死循环。
为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用HashMap,因为多线程下使用HashMap还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap。
HashMap线程不安全也是因为它可能会造成死循环与数据丢失问题。
16. ConcurrentHashMap的底层实现,如何保证线程安全的?
JDK1.8之前:
由Segment数组结构和HashEntry数据结构组成。
首先将数据分为一段一段(这个“段”就是 Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的个数一旦初始化就不能改变。Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。Segment 的结构和 HashMap 类似,是一种数组+链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的段的锁。也就是说,对同一段的并发写会被阻塞,不同段的写可以并发执行。
JDK1.8之后:
ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
Java 8 中,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
JDK1.8前后,ConcurrentHashMap的区别:
17. ConcurrentHashMap中不允许键和值为null。
这是为了避免二义性。并且单线程下可以容忍歧义,而多线程下无法容忍。
18. ConcurrentHashMap中复合操作的原子性。
复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(Key),然后根据结果进行插入或更新put(key,value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
ConcurrentHashMap提供了一些原子性的复合操作,如putIfAbsent、compute、computeIfPresent、merge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。