2.Java基础(16)
-
介绍Integer缓存,比较的时候要注意什么?(美团)
答:在Java中,Integer缓存是一个用于优化性能的特性,主要涉及到以下几点:
-
范围:Java会缓存-128到127之间的Integer对象。超出这个范围的Integer对象不会被缓存,每次创建都会生成一个新的对象。
-
使用
Integer.valueOf():建议使用Integer.valueOf(int)方法来获取Integer对象,因为它会优先返回缓存的对象,而直接使用new Integer(int)会创建新的对象。 -
性能考虑:由于缓存,频繁使用-128到127之间的整数时性能较好,而超出这个范围时性能会下降,因为每次都会创建新对象。
-
自动装箱:在进行自动装箱时,建议注意数值范围,避免不必要的对象创建。
-
==与equals的区别 :在比较两个Integer对象时,如果它们在缓存范围内,使用
==比较会返回true;如果超出范围,==可能返回false,而equals()方法则会比较实际的值,返回true。因为==比较的是变量的值或者引用的地址值,而equals()比较的是对象的内容。
-
-
ArrayList和LinkedList的区别,以及从插入删除效率上进行比较,如果是很大的数据量,从中间插入谁更快?这两个List的底层插入是如何做的知道吗?ArrayList会有线程安全问题吗?如果我想用ArrayList还想保证线程安全怎么做?线程安全的List有哪些?(滴滴)
答:ArrayList和LinkedList的主要区别在于底层数据结构:ArrayList基于动态数组,LinkedList基于链表。
-
插入和删除效率:在ArrayList中,从中间插入或删除元素需要移动后面的元素,时间复杂度为O(n)。而在LinkedList中,插入和删除操作只需调整指针,时间复杂度为O(1),但寻找插入位置的时间复杂度是O(n)。所以,对于大数据量,LinkedList在中间插入时更快。
-
底层实现:ArrayList使用数组存储元素,增加时可能会进行数组扩容;LinkedList使用节点,每个节点包含数据和指向前后节点的指针。
-
线程安全 :ArrayList不是线程安全的,多个线程并发修改会导致数据不一致。要保证线程安全,可以使用
Collections.synchronizedList()方法或CopyOnWriteArrayList。 -
线程安全的List :常用的线程安全的List有
Vector、CopyOnWriteArrayList和通过Collections.synchronizedList()包装的ArrayList。
-
-
HashMap原理(如何实现的)?HashMap的阈值为什么设置成0.75?初始容量为什么是16?(滴滴、B站)
HashMap是基于哈希表实现的,用于存储键值对。其工作原理和设计考虑包括以下几个方面:
1.原理
- 哈希函数:HashMap通过哈希函数将键映射到数组的索引位置。每个键在放入HashMap时,会通过哈希函数计算出一个哈希值,然后使用这个哈希值计算出数组的下标。
- 冲突解决:由于不同的键可能会被哈希到同一个索引,HashMap使用链表(在Java 8及以上版本中,当链表长度超过一定阈值会转换为红黑树)来解决冲突。在同一索引处,多个键值对会以链表或红黑树的形式存储。
2.阈值设置为0.75
- 负载因子:阈值(load factor)是决定何时需要扩容的指标。0.75是一个折中的选择,能够在时间复杂度和空间复杂度之间取得良好的平衡。低于0.75时,查找效率较高;超过0.75可能导致较高的冲突率,从而影响性能。
-
低于0.75时,HashMap中的元素分布相对稀疏,哈希表中的空槽较多,导致冲突较少。这意味着查找、插入和删除操作可以在平均O(1)时间内完成。一旦超过0.75,元素开始密集堆积,冲突率增加,多个元素可能会映射到相同的索引。此时,冲突解决机制(如链表或红黑树)需要处理这些冲突,导致查找和插入的时间复杂度上升,平均性能会变差,可能达到O(n)的最坏情况。因此,保持负载因子在0.75以下可以提高HashMap的整体效率。
可以这样背:在哈希表中,阈值越低就意味着在hashmap元素比较稀疏的时候就扩容了,这样元素的查找、插入和删除的效率比较高,但是阈值越低,所占用的空间也越多。阈值越高,意味着hashmap中元素堆积密集时才扩容,哈希冲突变多,链表和红黑树会导致查找和插入的时间复杂度上升,平均性能变成。而0.75可以平衡时间复杂度和空间复杂度
3.**初始容量为16 **
- 性能考虑:初始容量设置为16是为了在大多数情况下减少扩容次数。因为哈希表在扩容时需要重新计算所有元素的位置,时间复杂度为O(n)。选择16作为初始容量可以在多数应用场景下提供合理的性能,避免过早的扩容。
总结
- 性能优化:HashMap的设计是为了在空间和时间复杂度之间找到一个合理的平衡,以提供快速的查找和插入操作。
- 扩容策略:当实际存储的元素超过当前容量与负载因子的乘积时,会触发扩容,容量会翻倍,并重新哈希现有的元素。
这些设计选择确保了HashMap在大多数情况下都能高效地运行。
-
解决哈希冲突的策略?(腾讯、滴滴)扰动函数/hash函数的原理?扩容机制原理?(滴滴)
答:
1.解决哈希冲突的主要策略:
(1)开放寻址法
线性探测:在冲突发生时,检查下一个连续的索引(i + 1、i + 2,依此类推),直到找到空槽。这种方法容易导致聚集,形成"堆积"现象,影响性能。
二次探测:与线性探测类似,但使用平方的增量来探测空槽,例如i + 1²、i + 2²。这样可以减轻聚集问题。
双重哈希:使用两个哈希函数,首先计算主哈希值,再根据第二个哈希函数计算探测增量,从而找到空槽。这样可以提供更好的分散性。
(2)链地址法
每个索引位置存储一个链表或其他结构(如红黑树)。当多个键映射到同一索引时,它们会被追加到对应的链表中。在Java 8及以上版本中,如果某个链表的长度超过8,就会转换为红黑树,以提高查找效率。
2.扰动函数/hash函数的原理 https://www.zhihu.com/question/20733617
java//Java 8中的散列值优化函数 static final int hash(Object key) { int h; return (key == null) ? 0: (h = key.hashCode()) ^ (h >> 16); //key.hashCode()为哈希算法,返回初始哈希值 } key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。理论上 hash 散列是一个 int 值,如果直接拿出来作为下标访问 hashmap 的话,考虑到二进制 32 位,取值范围在**-2147483648 ~ 2147483647**。大概有 40 亿个 key , 只要哈希函数映射比较均匀松散,一般很难出现碰撞。但是要存下 40 亿长度的数组,服务器内存是不能放下的。通常HashMap 的默认长度为 16 。所以这个 hashCode (key.hashCode ) 是不能直接来使用的。使用之前先对数组长度取模,得到的余数才能用来访问数组下标。源码中的模运算是在这个indexFor()函数里完成的,把散列值和数组长度做一个"与"运算。
bucketIndex = indexFor(hash, table.length); static int indexFor(int h, int length) { return h & (length - 1); } 为什么要使用 length -1 来进行与运算,这里相当于是一个"低位掩码", 以默认长度 16 为例子。和某个数进行与运算,结果的大小是 < 16 的。如下所示:
10000000 00100000 00001001 & 00000000 00000000 00001111 ------------------------------ 00000000 00000000 00001001 // 高位全部归 0, 只保留后四位 但是如果本身的散列值分布松散,只是取后面几位的话,碰撞也会非常严重。还有如果散列本身做得不好的话,分布上成等差数列的漏洞,可能出现最后几位出现规律性的重复。这时候扰动函数的价值就体现出来了。

在 hash 函数中有这样的一段代码:(h = key.hashCode()) ^ (h >>> 16) 右位移 16 位, 正好是32bit 的一半,与自己的高半区做成异或,就是为了**混合原始的哈希码的高位和低位,以此来加大低位的随机性。**并且混合后的低位掺杂了高位的部分特征,这样高位的信息变相保存下来。其实按照开发经验来说绝大多数情况使用的时候 HashMap 的长度不会超过 1000,所以提升低位的随机性可以提升可以减少 hash 冲突,提升程序性能。
总结:
(1)hashCode() 求出哈希码(一个int型散列值 h)
(2)扰动函数:h 右位移16位,与自己的高半区做异或运算,达到混合哈希码高低位的目的(Hash = h ^ (h >>> 16))
(3)将扰动函数得到的哈希码,对数组长度取模运算(其实就是低位掩码,n-1 & h),得到的余数才是最终的索引位置。
3.扩容机制的原理
- 当元素数量超过阈值时,HashMap会进行扩容,通常将容量翻倍。扩容时,所有现有元素的哈希值需要重新计算并放入新的数组中。这是因为数组大小变化后,原有的哈希值可能不再适合新的索引位置,从而减少冲突。
-
==和eqauls;如何判断当前两个对象是否一样?(美团)
1.==运算符
- 比较引用 :
==用于比较两个对象的引用是否相同,即它们是否指向同一个内存地址。 - 基本数据类型 :对于基本数据类型(如
int、char等),==比较它们的值。
2.equals方法
- 比较内容 :
equals()方法用于比较两个对象的内容是否相等。默认情况下,Object类的equals()方法也比较引用,但许多类(如String、Integer)重写了这个方法,以提供内容比较的功能。
3.总结
- 使用
==比较引用,适用于检查两个对象是否是同一个实例。 - 使用
equals()比较内容,适用于检查两个对象是否表示相同的内容。在调用equals()之前,最好先检查对象是否为null,以避免NullPointerException。
- 比较引用 :
-
重写和重载,你是如何理解的?(美团)
1.重写
- 在继承关系中,子类重写父类的方法(签名参数必须相同;方法签名:方法名、返回值类型、参数)
- 修饰符:子类大于等于父类修饰符 public > protected > default > private,子类的访问级别不能低于父类。
- 异常抛出:父类有抛出异常,子类抛出的异常<=父类抛出的异常;父类没有抛出异常,子类只能抛出运行时异常
- 重写是一个运行期间概念,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法。
- 目的:实现多态性,通过父类引用调用子类对象的方法,实现对同一方法名的不同实现。
2.重载
- 在同一个类中,同名不同参(参数个数、参数类型、参数顺序可以不同)对访问修饰没有特殊要求
- 重载是一个编译期概念,即在编译时根据参数变量的类型判断应该调用哪个方法。
- 目的:增加方法的灵活性和可读性,让同一个方法名可以对不同情况进行处理。
3.总结
-
重写用于子类修改父类的方法实现,支持多态性。
-
重载允许同一方法名在同一类中有多个不同参数的实现,提高代码的灵活性和可读性。
-
如何复制一个对象?及如何解决这个对象复制问题?需要考虑哪些点?你可以用其他的方法吗?(美团)
1.浅复制/深复制
- 浅复制会创建一个新对象,但对象的字段仍然引用原始对象的引用类型字段。可以使用
clone()方法来实现。 - 深复制会创建一个新对象,并复制所有引用类型字段所指向的对象。这需要手动实现,例如通过序列化。
2.考虑点
-
可变性:如果对象包含可变字段,需谨慎选择深复制还是浅复制。
-
性能:深复制可能会影响性能,尤其是对象层次复杂时。
-
序列化 :确保要复制的对象实现
Serializable接口,以便进行序列化。
3.其他方法
- 构造函数复制:通过构造函数创建新对象,手动复制字段。
- 工具库 :使用像Apache Commons Lang的
SerializationUtils.clone()方法来简化深复制的实现。
- 浅复制会创建一个新对象,但对象的字段仍然引用原始对象的引用类型字段。可以使用
-
hashMap、hashSet区别?(滴滴)HashMap和Hashtable的区别?(美团)
1.hashMap、hashSet区别
HashSet底层就是基于HashMap实现的。(HashSet的源码非常非常少,因为除了clone()、writeObject()、readObject()是HashSet自己不得不实现之外,其他方法都是直接调用HashMap中的方法。
HashMap HashSet 实现了 Map接口实现 Set接口存储键值对 仅存储对象 调用 put()向map中添加元素调用 add()方法向Set中添加元素HashMap使用键(Key)计算hashcodeHashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性2.HashMap和Hashtable的区别
-
线程是否安全与效率:
HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过synchronized修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap吧!);因为线程安全的问题,HashMap要比Hashtable效率高一点。另外,Hashtable基本被淘汰,不要在代码中使用它; -
对 Null key 和 Null value 的支持:
HashMap可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException。 -
初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,
Hashtable默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为 2 的幂次方大小(HashMap中的tableSizeFor()方法保证,下面给出了源代码)。也就是说HashMap总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -
底层数据结构: JDK1.8 以后的
HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树 ),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable没有这样的机制。 -
哈希函数的实现 :
HashMap对哈希值进行了高位和低位的混合扰动处理以减少冲突,而Hashtable直接使用键的hashCode()值。
-
Hashtable和ConcurrentHashMap的区别?(众安、百度)你如何定义线程不安全,什么时候hashMap会出现线程不安全的问题?举一个实例的例子说明在多线程的情况下concurrentHashMap进行了什么样的动作保证了线程的安全性?(百度)
1.Hashtable和ConcurrentHashMap的区别
ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。-
ConcurrentHashMap实现线程安全的方式(重要):
- JAVA 7中ConcurrentHashMap的底层结构,它基本延续了HashMap的设计,采用的是数组 加 链表的形式。和HashMap不同的是,ConcurrentHashMap中的数组设计 分为大数组Segment和小数组HashEntry。大数组Segment可以理解为一个数据库,而每个数据库(Segment)中又有一个数组,数组中有很多HashEntry,每个HashEntry中又有一条链表。因为Segment本身是基于**ReentrantLock重入锁**实现的加锁和释放锁的操作,这样就能保证多个线程同时访问ConcurrentHashMap时,同一时间只能有一个线程能够操作相应的节点,这样就保证了ConcurrentHashMap的线程安全。也就是说 ConcurrentHashMap 的线程安全是建立在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片段锁。

- 在 JAVA8 中 ConcurrentHashMap 使用的是 CAS + volatile 或 synchronized 的方式来保证线程安全的。添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化。如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用 CAS 设置该节点;如果不为空则使用 synchronized 加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。
- 总结: Java 8中,ConcurrentHashMap 是在头节点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。

Hashtable(同一把锁) :使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

2.你如何定义线程不安全,什么时候hashMap会出现线程不安全的问题?
线程不安全:在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
JDK1.7 及之前版本,在多线程环境下,
HashMap扩容时会造成死循环和数据丢失的问题。(1)死循环
JDK1.7 及之前版本的
HashMap在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用
HashMap,因为多线程下使用HashMap还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap。(2)数据覆盖
JDK 1.8 后,在
HashMap中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对HashMap的put操作会导致线程不安全,具体来说会有数据覆盖的风险。举个例子:
- 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
- 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
- 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。
还有一种情况是这两个线程同时
put操作导致size的值不正确,进而导致数据覆盖的问题:- 线程 1 执行
if(++size > threshold)判断时,假设获得size的值为 10,由于时间片耗尽挂起。 - 线程 2 也执行
if(++size > threshold)判断,获得size的值也为 10,并将元素插入到该桶位中,并将size的值更新为 11。 - 随后,线程 1 获得时间片,它也将元素放入桶位中,并将 size 的值更新为 11。
- 线程 1、2 都执行了一次
put操作,但是size的值只增加了 1,也就导致实际上只有一个元素被添加到了HashMap中。
3.举一个实例的例子说明在多线程的情况下concurrentHashMap进行了什么样的动作保证了线程的安全性?
在 JDK 1.8 中 ConcurrentHashMap 使用的是 CAS + volatile 或 synchronized 的方式来保证线程安全的。添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化。如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用 CAS 设置该节点;如果不为空则使用 synchronize 加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。我们把上述流程简化一下,我们可以简单的认为在 JDK 1.8 中,ConcurrentHashMap 是在头节点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。
-
-
说一下java常见的基本数据类型,基本数据类型都能找到对应的包装类为什么这么设计?为什么不直接使用包装类,还要用基本数据类型,为什么不直接禁止掉?(百度)包装类的缓存机制了解吗?
1.基本数据类型:整型(byte 8、short 16、int 32、long 64)、浮点型(float 32、double 64)、字符型(char 16)、布尔型(boolean)。
2.包装类 :
Byte、Short、Integer、Long、Float、Double、Character、Boolean3.为什么设计包装类?
- 面向对象特性:包装类使基本数据类型能够作为对象使用,提供了更强的灵活性和功能性。
- 集合支持:Java的集合框架(如
ArrayList,HashMap等)只能存储对象,因此包装类允许基本数据类型以对象形式存储在集合中。 - 自动装箱与拆箱:包装类支持自动装箱(将基本类型转换为对应的对象)和拆箱(将对象转换为基本类型),简化了代码编写。
- 扩展功能:包装类提供了许多有用的方法,例如转换、比较、常量定义等,增强了基本类型的功能。
- null值处理:包装类可以为基本数据类型提供
null值,便于在某些情况下表示缺失或未初始化的状态。
4.为什么不直接使用包装类,还要用基本数据类型,为什么不直接禁止掉?
- 性能:不同的场景需要不同的类型,基本数据类型因为不涉及对象创建,所以使用起来更快,更轻量级,特别在大量数值计算时。
- 向后兼容性:禁止基本数据类型将破坏现有的代码,同时提供更灵活的数据类型。
- 自动装箱和拆箱:Java 5引入了自动装箱和拆箱的特性,使得在基本数据类型和包装类之间可以方便地转换,这样可以根据需要选择使用。
5.包装类的缓存机制
-
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character创建了数值在 [0,127] 范围的缓存数据,Boolean直接返回TrueorFalse。如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。 -
两种浮点数类型的包装类
Float,Double并没有实现缓存机制。
下面我们来看一个问题:下面的代码的输出结果是
true还是false呢?javaInteger i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);Integer i1=40这一行代码会发生装箱,也就是说这行代码等价于Integer i1=Integer.valueOf(40)。因此,i1直接使用的是缓存中的对象。而Integer i2 = new Integer(40)会直接创建新的对象。因此,答案是
false。你答对了吗?记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
-
谈谈你对装箱和拆箱的理解?(美团)
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱 int n = i; //拆箱查看上面这两行代码对应的字节码,装箱其实就是调用了 包装类的
valueOf()方法,拆箱其实就是调用了xxxValue()方法。因此,
Integer i = 10等价于Integer i = Integer.valueOf(10)int n = i等价于int n = i.intValue();
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
javaprivate static long sum() { // 应该使用 long 而不是 Long Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i; return sum; } -
说一下你使用的stream的API;了解怎么实现的吗?(百度)
Stream API是 Java 8 引入的一个强大的特性,它提供了一种高级迭代器,支持函数式编程,使得对集合的操作更加简洁和高效。Stream API允许以声明式方式处理数据集合。1.常见的Stream API
- 创建Stream:
- Arrays.stream(T[] array):从数组创建流。
- 中间操作:
- filter(Predicate<? super T> predicate):过滤元素。
- map(Function<? super T,? extends R> mapper):将流中的每个元素映射到另一个对象。
- sorted() 或 sorted(Comparator<? super T> comparator):对流中的元素进行排序。
- 终止操作:
forEach(Consumer<? super T> action):对流中的每个元素执行操作。collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner):将流转换为其他形式(如集合)。count():返回流中元素的数量。
javaimport java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamExample { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); // 使用 Stream 进行过滤和收集 List<String> filteredNames = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList()); // 打印结果 System.out.println(filteredNames); // 输出: [Alice] } }javaimport java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamExample { public static void main(String[] args) { // 创建一个学生列表 List<Student> students = Arrays.asList( new Student("Alice", 85), new Student("Bob", 75), new Student("Charlie", 90), new Student("David", 60), new Student("Eva", 95) ); // 使用 Stream API 筛选成绩超过 80 分的学生,并生成包含他们姓名和成绩的字符串列表 List<String> highAchievers = students.stream() // 使用 filter 方法筛选成绩超过 80 分的学生 .filter(student -> student.getScore() > 80) // 使用 map 方法将每个学生映射为一个描述字符串 .map(student -> student.getName() + " scored " + student.getScore()) // 使用 collect 方法将结果收集到一个新的列表中 .collect(Collectors.toList()); // 输出结果 highAchievers.forEach(System.out::println); } }2.实现原理
- 创建Stream:
-
抽象类和接口区别?(滴滴)
1.共同点
- 实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
- 抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
2.区别
-
设计层面:抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
-
继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
-
成员变量 :接口中的成员变量只能是
public static final类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private,protected,public),可以在子类中被重新定义或赋值。 -
方法
-
Java 8 之前,接口中的方法默认是
public abstract,也就是只能有方法声明。自 Java 8 起,可以在接口中定义default(默认) 方法和static(静态)方法。 自 Java 9 起,接口可以包含private方法。接口方法仅仅描述方法能做什么,但是不指定如何去做,所以接口中的方法都是抽象的(abstract方法)接口中字段的修饰符:public static final(默认不写)。① public : 使接口的实现类可以使用该常量;② static :接口不涉及和任何具体实例相关的细节,因此接口没有构造方法,不能被实例化,没有实例变量,只有静态变量。③ final:接口中不可以定义变量,即定义的变量前都要加上final修饰,使之成为常量,且必须赋初始值!
-
抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。
参数 抽象类 接口 默认的方法实现 可以有默认的方法实现 接口完全是抽象的,根本不存在方法的实现 实现 子类使用extends关键字来继承抽象类,如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 构造器 抽象类可以有构造器 接口不能有构造器 与正常java类的区别 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 接口是完全不同的类型 访问修饰符 抽象方法可以有public、protected和default这些修饰符 接口方法默认修饰符是public。也可以使用default、static、private。 main方法 抽象方法可以有main方法并且我们可以运行它 接口没有main方法,因此我们不能运行它 多继承 抽象方法可以继承一个类和实现多个接口 接口只可以继承一个或多个其它接口 添加新方法 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类 -
3.接口和抽象类分别在什么时候使用?
-
如果拥有一些方法,并想让他们中的一些有默认的具体实现,请选择抽象类
-
如果想实现多重继承,那么请使用接口,由于java不支持多继承,子类不能继承多个类,但一个类可以实现多个接口,因此可以使用接口来解决。
-
如果基本功能在不断变化,那么就使用抽象类,如果使用接口,那么每次变更都需要相应的去改变实现该接口的所有类。
-
Object类方法有哪些?(美团)
在 Java 中,
Object类是所有类的根类,这意味着所有类都直接或间接地继承自Object类。Object类提供了一些基本的方法,这些方法可以在任何对象上使用。以下是Object类的一些主要方法:-
equals(Object obj):- 检查两个对象是否相等。默认实现比较的是对象的引用,但通常需要重写这个方法来提供实际的值比较。
-
hashCode():- 返回对象的哈希码值。通常与
equals()方法一起重写,以确保相等的对象有相同的哈希码。
- 返回对象的哈希码值。通常与
-
clone():- 创建并返回对象的一个副本。这是一个受保护的方法,需要在子类中重写并提供 public 或其他访问修饰符。
-
toString():- 返回对象的字符串表示。默认返回类名和对象的哈希码的无符号十六进制表示。
-
getClass():- 返回运行时类的
Class对象。
- 返回运行时类的
-
notify():- 唤醒在此对象监视器上等待的单个线程。
-
notifyAll():- 唤醒在此对象监视器上等待的所有线程。
-
wait():- 导致当前线程等待,直到另一个线程调用此对象的
notify()或notifyAll()方法。
- 导致当前线程等待,直到另一个线程调用此对象的
-
wait(long timeout):- 导致当前线程等待,直到另一个线程调用此对象的
notify()或notifyAll()方法,或者超过指定的时间量。
- 导致当前线程等待,直到另一个线程调用此对象的
-
wait(long timeout, int nanos):- 导致当前线程等待,直到另一个线程调用此对象的
notify()或notifyAll()方法,或者其他某个条件成立,或者超过指定的时间量。
- 导致当前线程等待,直到另一个线程调用此对象的
这些方法为所有对象提供了基本的操作,如同步、比较、字符串表示等。在实际开发中,
equals()、hashCode()和toString()是最常被重写的方法,以满足特定对象的比较和输出需求。 -
-
final关键字可以作用在哪里?(美团)
- 修饰变量、方法、类
- 用于变量时,表示变量不可重新赋值;
- 局部变量 : 当一个局部变量被声明为
final,它的值在初始化后不能被改变。 - 实例变量 : 如果一个类的实例变量被声明为
final,那么该变量只能在构造函数中被赋值,之后不能修改。 - 静态变量 : 如果静态变量被声明为
final,它必须在静态上下文中初始化,并且不能被修改。
- 局部变量 : 当一个局部变量被声明为
- 用于方法时,表示方法不可被子类重写;
- 用于类时,表示类不可被继承。
-
集合总体分类?线程安全的集合有哪些?(美团)
1.两大类:Collection、Map
-
Collection
- List : 有序可重复的集合,如
ArrayList,LinkedList,Vector. - Set : 不允许重复的集合,如
HashSet,TreeSet,LinkedHashSet. - Queue : 用于处理排队的元素,如
PriorityQueue,LinkedList(也可以作为 Queue).
- List : 有序可重复的集合,如
-
Map
- HashMap: 键值对集合,不保证顺序。
- TreeMap: 按自然顺序或指定 Comparator 排序的键值对集合。
- LinkedHashMap: 维护插入顺序的 HashMap。
2.线程安全的集合
-
Vector: 线程安全的动态数组实现。
-
Stack: 线程安全的后进先出(LIFO)栈。
-
Hashtable: 线程安全的哈希表。
-
Collections.synchronizedList(): 将普通 List 转换为线程安全的 List
-
Collections.synchronizedSet(): 将普通 Set 转换为线程安全的 Set
-
Collections.synchronizedMap(): 将普通 Map 转换为线程安全的 Map
-
CopyOnWriteArrayList: 线程安全的 List 实现,适用于读多写少的场景
写时复制(Copy-On-Write) : 每当对
CopyOnWriteArrayList进行写操作(如add(),remove())时,它会创建底层数组的一个新副本,然后在新副本上进行修改。这意味着读操作可以在没有加锁的情况下并发进行,因为读操作始终访问的是不变的数组副本。这使得CopyOnWriteArrayList在高并发读取场景下表现优良。 -
ConcurrentHashMap: 高效的线程安全 Map,实现分段锁机制。
-