Java后端大厂高频面经——Java基础

2.Java基础(16)

  1. 介绍Integer缓存,比较的时候要注意什么?(美团)

    答:在Java中,Integer缓存是一个用于优化性能的特性,主要涉及到以下几点:

    1. 范围:Java会缓存-128到127之间的Integer对象。超出这个范围的Integer对象不会被缓存,每次创建都会生成一个新的对象。

    2. 使用Integer.valueOf() :建议使用Integer.valueOf(int)方法来获取Integer对象,因为它会优先返回缓存的对象,而直接使用new Integer(int)会创建新的对象。

    3. 性能考虑:由于缓存,频繁使用-128到127之间的整数时性能较好,而超出这个范围时性能会下降,因为每次都会创建新对象。

    4. 自动装箱:在进行自动装箱时,建议注意数值范围,避免不必要的对象创建。

    5. ==与equals的区别 :在比较两个Integer对象时,如果它们在缓存范围内,使用==比较会返回true;如果超出范围,==可能返回false,而equals()方法则会比较实际的值,返回true。

      因为==比较的是变量的值或者引用的地址值,而equals()比较的是对象的内容。

  2. ArrayList和LinkedList的区别,以及从插入删除效率上进行比较,如果是很大的数据量,从中间插入谁更快?这两个List的底层插入是如何做的知道吗?ArrayList会有线程安全问题吗?如果我想用ArrayList还想保证线程安全怎么做?线程安全的List有哪些?(滴滴)

    答:ArrayList和LinkedList的主要区别在于底层数据结构:ArrayList基于动态数组,LinkedList基于链表。

    1. 插入和删除效率:在ArrayList中,从中间插入或删除元素需要移动后面的元素,时间复杂度为O(n)。而在LinkedList中,插入和删除操作只需调整指针,时间复杂度为O(1),但寻找插入位置的时间复杂度是O(n)。所以,对于大数据量,LinkedList在中间插入时更快。

    2. 底层实现:ArrayList使用数组存储元素,增加时可能会进行数组扩容;LinkedList使用节点,每个节点包含数据和指向前后节点的指针。

    3. 线程安全 :ArrayList不是线程安全的,多个线程并发修改会导致数据不一致。要保证线程安全,可以使用Collections.synchronizedList()方法或CopyOnWriteArrayList

    4. 线程安全的List :常用的线程安全的List有VectorCopyOnWriteArrayList和通过Collections.synchronizedList()包装的ArrayList。

  3. 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在大多数情况下都能高效地运行。

  1. 解决哈希冲突的策略?(腾讯、滴滴)扰动函数/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会进行扩容,通常将容量翻倍。扩容时,所有现有元素的哈希值需要重新计算并放入新的数组中。这是因为数组大小变化后,原有的哈希值可能不再适合新的索引位置,从而减少冲突。
  2. ==和eqauls;如何判断当前两个对象是否一样?(美团)

    1.==运算符

    • 比较引用==用于比较两个对象的引用是否相同,即它们是否指向同一个内存地址。
    • 基本数据类型 :对于基本数据类型(如intchar等),==比较它们的值。

    2.equals方法

    • 比较内容equals()方法用于比较两个对象的内容是否相等。默认情况下,Object类的equals()方法也比较引用,但许多类(如StringInteger)重写了这个方法,以提供内容比较的功能。

    3.总结

    • 使用==比较引用,适用于检查两个对象是否是同一个实例。
    • 使用equals()比较内容,适用于检查两个对象是否表示相同的内容。在调用equals()之前,最好先检查对象是否为null,以避免NullPointerException
  3. 重写和重载,你是如何理解的?(美团)

    1.重写

    • 在继承关系中,子类重写父类的方法(签名参数必须相同;方法签名:方法名、返回值类型、参数)
    • 修饰符:子类大于等于父类修饰符 public > protected > default > private,子类的访问级别不能低于父类。
    • 异常抛出:父类有抛出异常,子类抛出的异常<=父类抛出的异常;父类没有抛出异常,子类只能抛出运行时异常
    • 重写是一个运行期间概念,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法。
    • 目的:实现多态性,通过父类引用调用子类对象的方法,实现对同一方法名的不同实现。

    2.重载

    • 在同一个类中,同名不同参(参数个数、参数类型、参数顺序可以不同)对访问修饰没有特殊要求
    • 重载是一个编译期概念,即在编译时根据参数变量的类型判断应该调用哪个方法。
    • 目的:增加方法的灵活性和可读性,让同一个方法名可以对不同情况进行处理。

    3.总结

    • 重写用于子类修改父类的方法实现,支持多态性。

    • 重载允许同一方法名在同一类中有多个不同参数的实现,提高代码的灵活性和可读性。

  4. 如何复制一个对象?及如何解决这个对象复制问题?需要考虑哪些点?你可以用其他的方法吗?(美团)

    1.浅复制/深复制

    • 浅复制会创建一个新对象,但对象的字段仍然引用原始对象的引用类型字段。可以使用clone()方法来实现。
    • 深复制会创建一个新对象,并复制所有引用类型字段所指向的对象。这需要手动实现,例如通过序列化。

    2.考虑点

    • 可变性:如果对象包含可变字段,需谨慎选择深复制还是浅复制。

    • 性能:深复制可能会影响性能,尤其是对象层次复杂时。

    • 序列化 :确保要复制的对象实现Serializable接口,以便进行序列化。

    3.其他方法

    • 构造函数复制:通过构造函数创建新对象,手动复制字段。
    • 工具库 :使用像Apache Commons Lang的SerializationUtils.clone()方法来简化深复制的实现。
  5. 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() 值。

  6. Hashtable和ConcurrentHashMap的区别?(众安、百度)你如何定义线程不安全,什么时候hashMap会出现线程不安全的问题?举一个实例的例子说明在多线程的情况下concurrentHashMap进行了什么样的动作保证了线程的安全性?(百度)

    1.Hashtable和ConcurrentHashMap的区别

    ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同。

    • 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),并以链表或红黑树的形式存储。多个线程对 HashMapput 操作会导致线程不安全,具体来说会有数据覆盖的风险。

    举个例子:

    • 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
    • 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
    • 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。

    还有一种情况是这两个线程同时 put 操作导致 size 的值不正确,进而导致数据覆盖的问题:

    1. 线程 1 执行 if(++size > threshold) 判断时,假设获得 size 的值为 10,由于时间片耗尽挂起。
    2. 线程 2 也执行 if(++size > threshold) 判断,获得 size 的值也为 10,并将元素插入到该桶位中,并将 size 的值更新为 11。
    3. 随后,线程 1 获得时间片,它也将元素放入桶位中,并将 size 的值更新为 11。
    4. 线程 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) 的时间复杂度。

  7. 说一下java常见的基本数据类型,基本数据类型都能找到对应的包装类为什么这么设计?为什么不直接使用包装类,还要用基本数据类型,为什么不直接禁止掉?(百度)包装类的缓存机制了解吗?

    1.基本数据类型:整型(byte 8、short 16、int 32、long 64)、浮点型(float 32、double 64)、字符型(char 16)、布尔型(boolean)。

    2.包装类ByteShortIntegerLongFloatDoubleCharacterBoolean

    3.为什么设计包装类?

    • 面向对象特性:包装类使基本数据类型能够作为对象使用,提供了更强的灵活性和功能性。
    • 集合支持:Java的集合框架(如ArrayList, HashMap等)只能存储对象,因此包装类允许基本数据类型以对象形式存储在集合中。
    • 自动装箱与拆箱:包装类支持自动装箱(将基本类型转换为对应的对象)和拆箱(将对象转换为基本类型),简化了代码编写。
    • 扩展功能:包装类提供了许多有用的方法,例如转换、比较、常量定义等,增强了基本类型的功能。
    • null值处理:包装类可以为基本数据类型提供null值,便于在某些情况下表示缺失或未初始化的状态。

    4.为什么不直接使用包装类,还要用基本数据类型,为什么不直接禁止掉?

    • 性能:不同的场景需要不同的类型,基本数据类型因为不涉及对象创建,所以使用起来更快,更轻量级,特别在大量数值计算时。
    • 向后兼容性:禁止基本数据类型将破坏现有的代码,同时提供更灵活的数据类型。
    • 自动装箱和拆箱:Java 5引入了自动装箱和拆箱的特性,使得在基本数据类型和包装类之间可以方便地转换,这样可以根据需要选择使用。

    5.包装类的缓存机制

    • Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

    • 两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

    下面我们来看一个问题:下面的代码的输出结果是 true 还是 false 呢?

    java 复制代码
    Integer 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 方法比较

  8. 谈谈你对装箱和拆箱的理解?(美团)

    • 装箱:将基本类型用它们对应的引用类型包装起来;
    • 拆箱:将包装类型转换为基本数据类型;

    举例:

    Integer i = 10;  //装箱
    int n = i;   //拆箱
    

    查看上面这两行代码对应的字节码,装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。

    因此,

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

    注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

    java 复制代码
    private static long sum() {
        // 应该使用 long 而不是 Long
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++)
            sum += i;
        return sum;
    }
  9. 说一下你使用的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():返回流中元素的数量。
    java 复制代码
    import 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]
        }
    }
    java 复制代码
    import 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.实现原理

  10. 抽象类和接口区别?(滴滴)

    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不支持多继承,子类不能继承多个类,但一个类可以实现多个接口,因此可以使用接口来解决。

    • 如果基本功能在不断变化,那么就使用抽象类,如果使用接口,那么每次变更都需要相应的去改变实现该接口的所有类。

  11. Object类方法有哪些?(美团)

    在 Java 中,Object 类是所有类的根类,这意味着所有类都直接或间接地继承自 Object 类。Object 类提供了一些基本的方法,这些方法可以在任何对象上使用。以下是 Object 类的一些主要方法:

    1. equals(Object obj)

      • 检查两个对象是否相等。默认实现比较的是对象的引用,但通常需要重写这个方法来提供实际的值比较。
    2. hashCode()

      • 返回对象的哈希码值。通常与 equals() 方法一起重写,以确保相等的对象有相同的哈希码。
    3. clone()

      • 创建并返回对象的一个副本。这是一个受保护的方法,需要在子类中重写并提供 public 或其他访问修饰符。
    4. toString()

      • 返回对象的字符串表示。默认返回类名和对象的哈希码的无符号十六进制表示。
    5. getClass()

      • 返回运行时类的 Class 对象。
    6. notify()

      • 唤醒在此对象监视器上等待的单个线程。
    7. notifyAll()

      • 唤醒在此对象监视器上等待的所有线程。
    8. wait()

      • 导致当前线程等待,直到另一个线程调用此对象的 notify()notifyAll() 方法。
    9. wait(long timeout)

      • 导致当前线程等待,直到另一个线程调用此对象的 notify()notifyAll() 方法,或者超过指定的时间量。
    10. wait(long timeout, int nanos)

      • 导致当前线程等待,直到另一个线程调用此对象的 notify()notifyAll() 方法,或者其他某个条件成立,或者超过指定的时间量。

    这些方法为所有对象提供了基本的操作,如同步、比较、字符串表示等。在实际开发中,equals()hashCode()toString() 是最常被重写的方法,以满足特定对象的比较和输出需求。

  12. final关键字可以作用在哪里?(美团)

    • 修饰变量、方法、类
    • 用于变量时,表示变量不可重新赋值;
      • 局部变量 : 当一个局部变量被声明为 final,它的值在初始化后不能被改变。
      • 实例变量 : 如果一个类的实例变量被声明为 final,那么该变量只能在构造函数中被赋值,之后不能修改。
      • 静态变量 : 如果静态变量被声明为 final,它必须在静态上下文中初始化,并且不能被修改。
    • 用于方法时,表示方法不可被子类重写;
    • 用于类时,表示类不可被继承。
  13. 集合总体分类?线程安全的集合有哪些?(美团)

    1.两大类:Collection、Map

    • Collection

      • List : 有序可重复的集合,如 ArrayList, LinkedList, Vector.
      • Set : 不允许重复的集合,如 HashSet, TreeSet, LinkedHashSet.
      • Queue : 用于处理排队的元素,如 PriorityQueue, LinkedList (也可以作为 Queue).
    • 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,实现分段锁机制。

相关推荐
欢乐少年19041 小时前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
夏天的味道٥2 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
IT、木易3 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
冰糖码奇朵4 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
好教员好4 小时前
【Spring】整合【SpringMVC】
java·spring
Mr.NickJJ4 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
浪九天5 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
Archer1945 小时前
C语言——链表
c语言·开发语言·链表
My Li.5 小时前
c++的介绍
开发语言·c++
堕落年代5 小时前
Maven匹配机制和仓库库设置
java·maven