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)计算hashcode
HashSet
使用成员对象来计算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
、Boolean
3.为什么设计包装类?
- 面向对象特性:包装类使基本数据类型能够作为对象使用,提供了更强的灵活性和功能性。
- 集合支持:Java的集合框架(如
ArrayList
,HashMap
等)只能存储对象,因此包装类允许基本数据类型以对象形式存储在集合中。 - 自动装箱与拆箱:包装类支持自动装箱(将基本类型转换为对应的对象)和拆箱(将对象转换为基本类型),简化了代码编写。
- 扩展功能:包装类提供了许多有用的方法,例如转换、比较、常量定义等,增强了基本类型的功能。
- null值处理:包装类可以为基本数据类型提供
null
值,便于在某些情况下表示缺失或未初始化的状态。
4.为什么不直接使用包装类,还要用基本数据类型,为什么不直接禁止掉?
- 性能:不同的场景需要不同的类型,基本数据类型因为不涉及对象创建,所以使用起来更快,更轻量级,特别在大量数值计算时。
- 向后兼容性:禁止基本数据类型将破坏现有的代码,同时提供更灵活的数据类型。
- 自动装箱和拆箱:Java 5引入了自动装箱和拆箱的特性,使得在基本数据类型和包装类之间可以方便地转换,这样可以根据需要选择使用。
5.包装类的缓存机制
-
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回True
orFalse
。如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。 -
两种浮点数类型的包装类
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,实现分段锁机制。
-