当前位置:网站首页 > Java基础 > 正文

java集合基础接口



image.png

小人-翻跟头.gif

前言

对Java集合框架的理解与应用,是Java开发工程师的基础能力之一,这篇文章中精选了一部分面试题,这些面试题完全涵盖到了Java集合框架的所有核心知识点。通过这些问题,可以很好地考察出候选人对Java集合框架的原理、使用场景、性能考量及高级特性等方面理解与应用,同时也能反映出候选人的实际编程能力java集合基础接口和问题解决能力。如果你正在准备相关面试,这篇文章绝对值得一读。文章内容有点长,建议先收藏起来,慢慢看。

基础概念与接口

Java集合框架的基础接口有哪些?它们之间的区别是什么?

Java集合框架的基础接口主要有以下几种,每种接口都有其特定的目的和用途:

  1. Collection接口
  • 这是所有集合类的根接口,定义了一组通用的操作,比如添加(add)、删除(remove)、检查是否包含某元素(contains)、大小(size)以及迭代(iterator)等方法。所有具体的集合类如List、Set都直接或间接实现了这个接口。
  1. List接口
  • List是一个有序集合,允许元素重复,且提供了按索引访问元素的能力。这意味着你可以通过元素的索引来精确地控制它们的位置,比如插入、更新或删除特定位置的元素。ArrayList和LinkedList是List接口的两个典型实现。
  1. Set接口
  • Set是一个不允许重复元素的集合,不保证元素的顺序(尽管某些实现如LinkedHashSet可以维护插入顺序)。它主要用于当你需要存储唯一元素时,比如存储一组不重复的名字或ID。HashSet和TreeSet是两种常见的Set实现,HashSet提供快速访问,而TreeSet则对元素进行了自然排序或基于比较器的排序。
  1. Map接口
  • Map是一种键值对的集合,它不直接继承自Collection接口,但同样属于集合框架的一部分。每个键都是唯一的,与之关联一个值。Map的主要操作包括根据键获取值(get)、插入键值对(put)、删除键值对(remove)等。HashMap、TreeMap和LinkedHashMap是Map接口的常见实现,各自有不同的性能特性和功能,如HashMap提供快速访问,TreeMap保证键的排序,而LinkedHashMap保持插入顺序或访问顺序。

区别

它们之间的区别主要在于数据结构、是否允许重复元素、是否保证元素顺序以及提供的额外操作上。简而言之,选择哪个接口或其实现取决于具体的应用需求,比如是否需要保证元素的唯一性、是否关心元素的顺序、是否需要高效的查找或插入操作等。

解释一下Collection与Map接口的主要区别。

Collection与Map接口的主要区别可以从以下几个方面来理解:

  1. 数据结构与存储方式
  • Collection:它主要设计用来存储一组独立的对象,这些对象可以是重复的(具体取决于实现,如List允许重复,而Set不允许),并且只关注元素本身。Collection的实现类如List、Set等,专注于元素的集合管理。
  • Map:而Map则是一种键值对的数据结构,它将一个独一无二的键(Key)与一个值(Value)关联起来。每个键在Map中只能对应一个值,这确保了键的唯一性。Map关注的是键值对的整体,而非单独的键或值。
  1. 访问方式
  • Collection:通过索引(如果是List)或直接通过元素(对于Set)来访问或操作元素。
  • Map:通过键来访问其对应的值,而不是直接通过索引或元素本身。
  1. 功能方法
  • Collection:提供了添加、删除、检查元素是否存在、遍历元素等操作。
  • Map:除了基本的添加、删除、检查键值对是否存在外,还提供了根据键获取值、获取所有键集、值集或键值对集的功能。
  1. 用途
  • Collection:更适合用于需要管理一群相同类型对象的场景,比如存储多个用户对象、商品对象等。
  • Map:更适合需要通过某个标识符(键)来快速查找相关联信息的场景,如数据库记录的缓存、配置参数的存储等。
  1. 继承关系
  • Collection是所有单值集合的根接口,下面直接派生出List、Set、Queue等接口。
  • Map是一个独立的接口,并不继承自Collection,它定义了键值对的存储和访问方式,与Collection并列,都是Java集合框架的核心部分。

总的来说,Collection关注元素集合,而Map关注键值对映射,两者在数据组织和操作方式上有着本质的不同。

谈谈Iterable接口的作用及其方法。

Iterable接口的作用

Iterable接口是Java集合框架中的一个核心接口,它位于java.lang包下。这个接口的设计目的是为了提供一种统一的方式去遍历任何集合类型(包括列表、集合、映射的键集或值集等),不论其底层数据结构如何。实现了Iterable接口的类能够使用Java的增强型for-each循环进行遍历,大大简化了集合遍历的代码书写,提高了代码的可读性和易用性。

Iterable接口的方法

尽管Iterable接口的设计目标是提供遍历功能,但它实际上只定义了一个核心方法:

  • Iterator<T> iterator():此方法返回一个实现了Iterator接口的对象。Iterator对象包含了遍历集合所需的方法,如hasNext()、next()和在某些情况下remove()等。hasNext()用于检查集合中是否还有更多的元素;next()用于获取下一个元素;而remove()则允许在迭代过程中移除元素(如果集合支持的话)。

Iterable接口的重要性

  • 通用遍历机制:通过Iterable接口,所有实现了它的集合类都可以使用相同的迭代逻辑,这极大地增强了代码的通用性和可重用性。
  • 支持foreach循环:因为Iterable接口的存在,Java的foreach循环能够工作在任何实现了Iterable的集合上,使得遍历集合的操作变得非常直观和简洁。
  • 灵活性与扩展性:集合类通过实现Iterable并提供自定义的Iterator,可以支持不同的遍历策略,比如正向遍历、反向遍历或是特定条件下的遍历,从而提升了集合框架的灵活性和扩展性。
  • Java 8新特性:从Java 8开始,Iterable接口还增加了新的默认方法,如forEach(Consumer action),它允许直接在Iterable上使用Lambda表达式进行遍历操作,进一步简化了集合的处理逻辑,并支持函数式编程风格。

总的来说,Iterable接口在Java集合框架中起到了基石的作用,为集合的遍历提供了一致的标准,是理解和使用Java集合框架不可或缺的一部分。

List接口与实现

列举并解释ArrayList、LinkedList和Vector的主要区别。

ArrayList、LinkedList和Vector是Java集合框架中实现List接口的三种常用数据结构,它们的主要区别如下:

  1. 数据结构
  • ArrayList:基于动态数组实现,内部使用一个可变大小的数组来存储元素。这意味着在列表的末尾添加或删除元素相对高效,但插入或删除中间的元素则较慢,因为可能需要移动后续的元素。
  • LinkedList:基于双向链表实现,每个节点包含元素本身和指向前后节点的引用。这种结构使得在列表的任何位置进行插入和删除操作都非常高效,但随机访问元素时性能较低,因为可能需要从头或尾开始遍历链表。
  • Vector:类似于ArrayList,也是基于动态数组实现,但在早期Java版本中设计为线程安全的,意味着它的许多方法都加了synchronized关键字以确保多线程环境下的安全性。这导致其在多线程访问时性能较差,而在单线程环境下与ArrayList相比并无明显优势。
  1. 线程安全
  • ArrayList:非线程安全,适合单线程环境或在外部手动同步的情况下使用。
  • LinkedList:同样是非线程安全的,适用于单线程环境。
  • Vector:线程安全,由于其内部方法的同步设计,可以在多线程环境下安全使用,但牺牲了部分性能。
  1. 性能特点
  • 随机访问:ArrayList由于是数组实现,提供了较快的随机访问速度,即通过索引访问元素的速度很快。
  • 插入/删除:LinkedList在插入和删除操作上通常表现得更优,尤其是当操作发生在列表的中间或开头时,因为它不需要移动其他元素。
  • 内存使用与扩容:ArrayList和Vector在初始化时都会有一个默认容量,当元素数量超过容量时,ArrayList默认增长为原来的1.5倍,而Vector默认增长为原来的2倍。LinkedList由于链表结构,每个元素额外存储了前后节点的引用,可能会占用更多内存。
  1. 使用场景
  • ArrayList:适用于大多数需要快速随机访问且不频繁插入删除的场景。
  • LinkedList:适合于频繁插入删除操作且对随机访问速度要求不高的场景,如实现队列、栈等。
  • Vector:由于线程安全的特性,它在多线程环境中使用较多,但考虑到性能因素,现代开发更倾向于使用Collections.synchronizedList包装ArrayList或LinkedList来获得线程安全。

总的来说,选择哪种数据结构应基于具体的应用场景和性能需求,考虑数据的访问模式(随机访问还是频繁插入删除)、线程安全需求以及内存使用等因素。

在什么场景下你会选择使用ArrayList而不是LinkedList?反之呢?

选择使用ArrayList而不是LinkedList的场景主要包括:

  1. 频繁的随机访问:如果你的程序需要经常根据索引快速查找元素,ArrayList是更好的选择,因为它提供了O(1)的时间复杂度进行随机访问。例如,当你需要频繁地读取列表中的元素,而不仅仅是遍历。
  2. 较少的插入和删除操作:当插入和删除操作相对较少,特别是不在列表开头或中间进行时,ArrayList的性能损失相对较小。它在列表末尾的添加和删除操作仍然是高效的。
  3. 内存使用考量:ArrayList通常比LinkedList更节省内存,因为LinkedList中的每个节点都需要额外的空间来存储指针。

相反,选择使用LinkedList而不是ArrayList的场景有:

  1. 频繁的插入和删除操作:特别是在列表的开头、中间进行插入和删除时,LinkedList表现更优,因为它只需要改变相邻节点的引用,时间复杂度为O(1),而ArrayList可能需要移动大量元素。
  2. 需要高效地插入和删除大量数据:如果应用需要在列表中间频繁执行插入和删除,并且对这些操作的性能敏感,LinkedList的常数时间复杂度优势更加明显。
  3. 迭代器的高效使用:当需要频繁地使用迭代器并在迭代过程中进行插入和删除操作时,LinkedList的迭代器在面对这些操作时更加稳定,不会抛出 ConcurrentModificationException,而ArrayList在迭代过程中修改列表可能会导致异常。
  4. 作为队列或栈使用:LinkedList天然适合实现队列(先进先出FIFO)和栈(后进先出LIFO)这样的数据结构,因为它可以非常高效地在两端添加或移除元素。

总的来说,选择ArrayList还是LinkedList应当基于具体的应用场景,考虑数据结构对性能的影响,以及操作的频率和类型。

如何实现一个线程安全的List?

在Java中实现一个线程安全的List,可以通过以下几种方式:

  1. 使用Vector

Vector是Java早期提供的线程安全的List实现,它通过在每个修改方法上加synchronized关键字来实现同步。然而,由于其锁定整个集合,当多线程同时访问时可能会导致性能瓶颈。

  1. 使用Collections.synchronizedList

如果你已经有了一个非线程安全的List实例(如ArrayList或LinkedList),可以通过Collections.synchronizedList(List<T> list)方法将其转换为线程安全的List。这个方法返回一个包装器类,它在所有修改操作上添加了同步锁。需要注意的是,虽然修改操作变得线程安全,但是如果要进行迭代操作,仍然需要手动同步或使用迭代器的hasNext()和next()方法时外部同步。

  1. 使用CopyOnWriteArrayList

CopyOnWriteArrayList是Java并发包提供的线程安全List实现,特别适合读多写少的场景。它通过在每次修改时复制整个底层数组来实现线程安全,这样读操作永远不会阻塞。这使得它非常适合用于迭代,即使在其他线程正在修改列表时也是如此,但频繁的写入操作会导致较高的内存消耗和性能开销。

  1. 自定义同步策略

可以自己实现一个List类,并在其中手动添加同步机制,如使用ReentrantLock或Semaphore等并发工具类来控制对集合的访问。这种方式提供了最大的灵活性,但也要求开发者对并发编程有深入的理解,以避免死锁和其他并发问题。

选择哪种方法取决于具体的需求,如并发访问的模式(读多还是写多)、对性能的要求以及是否可以接受数据的瞬时一致性等。在多线程环境下,通常推荐使用CopyOnWriteArrayList进行读取密集型操作,而对于写入频繁的情况,则需权衡性能与安全,可能考虑其他同步策略。

Set接口与实现

HashSet与TreeSet的工作原理分别是什么?它们在性能上有何不同?

HashSet的工作原理

HashSet是基于哈希表实现的,它利用哈希算法将元素映射到哈希表中的某个位置。当向HashSet中添加元素时,首先会计算元素的哈希值,然后根据哈希值将元素放置在一个桶(bucket)中。如果两个元素的哈希值相同(发生哈希冲突),则使用equals()方法进一步比较这两个元素是否相等,如果equals()返回true,则认为这两个元素重复,只保留一个;否则,它们会被放置在同一个桶的不同位置或相邻的桶中。HashSet不保证元素的顺序,这是因为哈希值的分布决定了元素的存储位置。

TreeSet的工作原理

TreeSet是基于红黑树实现的,红黑树是一种自平衡的二叉查找树。当向TreeSet中添加元素时,会根据元素的自然排序(实现Comparable接口)或者提供的比较器(Comparator)来确定元素在树中的位置,以保持元素的有序性。新添加的元素会在适当的位置插入,以维持树的平衡状态。TreeSet中的元素按照排序规则自动排序,可以是升序也可以是降序或其他自定义排序方式。

性能上的不同

  • 查找性能:在平均情况下,HashSet的查找、插入和删除操作的时间复杂度为O(1),这是由于哈希表良好的分散特性和快速定位能力。而TreeSet的这些操作在最坏情况下的时间复杂度为O(log n),因为需要沿着树的路径进行查找或调整树的平衡,但实际中由于红黑树的良好平衡性,性能往往接近**情况。
  • 排序与遍历:TreeSet由于其内部元素有序,所以在需要有序遍历或者快速查找特定排名的元素时,性能优于HashSet。对于需要排序的场景,TreeSet直接提供了有序访问的能力,而HashSet则不保证元素的顺序。
  • 内存消耗:HashSet通常比TreeSet具有更低的内存消耗,因为哈希表不需要额外的空间来维护元素间的排序关系。
  • 初始化及扩容成本:HashSet在初始化时和当容量需要扩容时,可能需要重新分配更大的数组并重新计算元素的哈希位置,这在极端情况下可能会带来一定的性能开销。而TreeSet由于是动态调整的树结构,每次插入元素时的结构调整相对较小,但维护平衡树结构也有一定的成本。

总的来说,HashSet适合于不需要维护元素间特定顺序且需要快速插入、删除和查找的场景,而TreeSet则适用于需要保持元素排序的场景,尽管在插入和删除操作上可能稍微慢一些,但它提供了排序遍历的优势。

如何确保一个Set中的元素唯一性?

确保Set中的元素唯一性是Set数据结构的固有特性。在Java中,Set接口的实现类如HashSet、TreeSet、LinkedHashSet等,都自动保证了元素的唯一性。以下是几种Set实现确保元素唯一性的方式及其背后的机制:

  1. HashSet
  • 哈希码和equals方法:HashSet使用哈希表(实际上是一个HashMap)来存储元素。当向HashSet中添加一个元素时,首先调用该元素的hashCode()方法生成一个哈希码,然后根据这个哈希码确定元素在哈希表中的桶(bucket)位置。如果有两个元素的哈希码相同(哈希冲突),HashSet会进一步调用这些元素的equals()方法来检查它们是否相等。如果equals()返回true,则认为这两个元素是相同的,HashSet只保留一个。
  • 重写equals和hashCode:为了正确地识别元素的唯一性,存入HashSet中的对象的类通常需要重写equals()和hashCode()方法,确保相等的对象(根据业务逻辑)有相同的哈希码,并且equals()方法返回正确的相等判断结果。
  1. TreeSet
  • 自然排序或比较器:TreeSet使用红黑树来存储元素,保证元素的自然排序或根据提供的Comparator进行排序。TreeSet在添加元素时,会根据比较结果找到元素在树中的正确位置。如果试图添加的元素与已存在元素根据排序规则比较相等,则添加操作失败,从而保证了元素的唯一性。
  • Comparable或Comparator:为了实现排序,存入TreeSet中的对象要么实现了Comparable接口(定义其自然排序),要么在创建TreeSet时提供了一个Comparator比较器。
  1. LinkedHashSet
  • 哈希码、equals方法与双向链表:LinkedHashSet继承自HashSet,因此它也使用哈希码和equals方法来确保元素的唯一性,同时通过维护一个双向链表来保持元素的插入顺序。它结合了HashSet的高性能和LinkedHashSet的插入顺序保持特性。

总结来说,确保Set中元素的唯一性主要依赖于哈希码(hashCode())和相等性比较(equals())的正确实现,以及在某些情况下(如TreeSet)的排序规则。开发者在设计存入Set中的对象时,需要确保这些方法被正确重写,以满足业务逻辑上的元素唯一性要求。

为什么LinkedHashSet能够保持插入顺序?

LinkedHashSet能够保持插入顺序的原因在于它的内部实现机制,该机制结合了哈希表(用于高效查找和确保唯一性)和双向链表(用于维护元素的插入顺序)的特点。具体来说:

  • 哈希表(HashSet的特性):LinkedHashSet底层使用哈希表数据结构,这意味着每个元素都会根据其hashCode()方法计算出的哈希值定位到表中的一个“桶”(bucket)。这样可以快速地执行添加、删除和查找操作。当发生哈希冲突时(即两个不同的元素计算出相同的哈希值),通过进一步调用equals()方法来确保唯一性,避免重复元素的加入。
  • 双向链表(LinkedList的特性):除了哈希表之外,LinkedHashSet还为每个元素维护了一个双向链表。每当有新元素添加到集合中时,这个元素不仅会被放置在哈希表的相应位置上,同时也会作为一个节点插入到双向链表的末尾。这样,即使哈希表中的元素因为哈希冲突而重新排列,双向链表依然保持着元素的原始插入顺序。
  • 插入与遍历:由于有了这个双向链表,当遍历LinkedHashSet时,就可以按照元素的插入顺序来进行,而不是按照哈希值或者自然排序的顺序。新元素总是添加到链表的尾部,因此遍历集合时会先遇到最早插入的元素,然后是之后依次插入的元素,保证了迭代顺序与插入顺序一致。

总的来说,LinkedHashSet通过结合哈希表的高效性和双向链表的顺序性,实现了既能确保元素的唯一性,又能保持元素插入顺序的目的。

Map接口与实现

HashMap、TreeMap和LinkedHashMap的区别是什么?

HashMap、TreeMap和LinkedHashMap都是Java中实现Map接口的集合类,用于存储键值对,但它们在内部实现、性能特点和功能上有所不同:

  1. HashMap:
  • 无序性: HashMap不保证键值对的顺序。它使用哈希表实现,根据键的哈希值存储元素,适合快速查询。
  • 允许null键和值: HashMap允许一个null键和多个null值。
  • 线程不安全: HashMap不是线程安全的,如果在多线程环境中不加同步控制地使用,可能会导致数据不一致。在需要线程安全时,可以使用ConcurrentHashMap。
  • 性能: 提供了O(1)的平均时间复杂度进行插入、删除和查找操作。
  1. TreeMap:
  • 有序性: TreeMap根据键的自然顺序或自定义比较器排序,保证键值对是有序的。默认按照键的自然顺序升序排序。
  • 不允许null键: TreeMap不允许键为null,但允许值为null(尽管不建议这样做,因为null值可能导致歧义)。
  • 线程不安全: 同HashMap,TreeMap也不是线程安全的。
  • 性能: 提供了O(log n)的时间复杂度进行插入、删除和查找操作,因为它是基于红黑树实现的。
  • 应用场景: 当需要按特定顺序(如排序)处理键值对时,TreeMap非常有用。
  1. LinkedHashMap:
  • 有序性: LinkedHashMap保持了键值对的插入顺序,或者最近最少使用(LRU)顺序(如果设置了访问顺序)。它内部维护了一个双向链表来保证这一特性。
  • 允许null键和值: 类似HashMap,LinkedHashMap也允许键和值为null。
  • 线程不安全: LinkedHashMap同样不是线程安全的。
  • 性能: 在性能上与HashMap相似,插入、删除和查找操作的平均时间复杂度为O(1)。
  • 应用场景: 当需要保持键值对的插入顺序或访问顺序时,例如实现缓存或需要有序遍历的情况,LinkedHashMap是理想选择。

总的来说,选择哪个集合类应基于具体需求:如果需要快速访问且不关心顺序,可选HashMap;如果需要按键排序,则应选用TreeMap;若要保持插入顺序或访问顺序,应使用LinkedHashMap。

HashMap的工作原理是什么?它是如何处理哈希冲突的?

HashMap的工作原理基于哈希表实现,它是Java集合框架中Map接口的一个非同步实现,允许使用null键和null值。HashMap的核心在于高效地存储和检索键值对,其内部结构是一个数组和链表(在JDK 1.8及以后版本中,当链表长度达到一定阈值时,会转换为红黑树以提高性能)的组合。

工作原理概览

  1. 哈希计算:当向HashMap中添加一个键值对时,首先会计算键的哈希值,通常使用键对象的hashCode()方法。这个哈希值随后会被映射到一个数组索引上,这个过程称为哈希化。
  2. 数组存储:HashMap内部维护了一个数组(称为桶数组),数组的大小通常是2的幂,以利于快速定位。哈希值通过某种算法(通常是取模运算,但在Java中采用了更复杂的位操作来提高效率)转换为数组内的索引。
  3. 链地址法解决冲突:如果两个或更多的键计算出相同的哈希值(即发生哈希冲突),HashMap会使用链地址法来解决冲突。这意味着在数组的同一索引位置上,会有一个链表(或在JDK 1.8中当链表长度超过8时转换为红黑树)来保存所有冲突的键值对。每个链表节点(或树节点)存储一个键值对。

处理哈希冲突的具体方式

  • 链表处理:在JDK 1.7及之前版本中,HashMap在遇到冲突时,会将新的键值对添加到链表的头部(头插法),这可能导致在极端情况下出现循环链表的问题。而在JDK 1.8中,改为尾插法,减少了这种风险。
  • 红黑树转换:从JDK 1.8开始,当链表长度超过8且数组长度大于等于64时,链表会转换为红黑树,以减少查找时间,提高性能。相反,当红黑树中的节点数量降到6以下时,又会退化回链表。

性能考虑

  • 负载因子:HashMap有一个负载因子,默认为0.75,当HashMap中元素的数量超过数组大小乘以负载因子时,HashMap会进行扩容,通常扩容为原大小的两倍,以减少哈希冲突,保持良好的性能。
  • 扰动函数:在计算最终索引位置时,HashMap会使用一个扰动函数进一步打乱哈希值,通过多次位运算和异或操作来增加哈希值的随机性,减少冲突概率。

总的来说,HashMap通过高效的哈希计算、数组索引定位、链表或红黑树解决冲突,以及适时的扩容机制,实现了快速的插入、删除和查找操作。

如何实现一个线程安全的Map?

在Java中,实现一个线程安全的Map可以通过以下几种方式:

  1. 使用ConcurrentHashMap

Java标准库中的ConcurrentHashMap是专为高并发环境设计的线程安全Map实现。它通过分段锁(Segment,直到Java 7)和之后的CAS(Compare-and-Swap)操作与锁粒度细化技术(从Java 8开始)来实现高效的线程安全。这意味着在大多数情况下,多个读操作可以并发进行,而写操作只影响相关部分,不会阻塞整个Map。使用方式非常直接:

 
  1. 使用Collections.synchronizedMap()

如果你已经有了一个非线程安全的Map(比如HashMap),可以使用Collections类的synchronizedMap()方法将其包装成线程安全的Map。这种方式通过在Map的方法上加锁来实现线程安全,但请注意,这会导致所有操作串行执行,影响并发性能。

 
  1. 使用Hashtable

虽然Hashtable是Java早期提供的线程安全Map实现,但由于其性能较ConcurrentHashMap差,且在同步策略上较为简单(对整个Map加锁),在现代应用中已较少推荐使用。不过,如果你需要一个绝对线程安全的Map,并且不介意较低的并发性能,可以考虑使用。

 
  1. 自定义实现

你还可以自定义一个线程安全的Map,通过在关键操作上使用锁或其他同步机制(如Semaphore、ReentrantLock等)来确保线程安全性。这种方式灵活性高,但需要深入理解并发编程和锁的使用,且容易出错。

通常,ConcurrentHashMap是首选,因为它在并发性能和线程安全性之间提供了很好的平衡。除非有特殊需求,否则不建议使用Hashtable或手动同步HashMap,因为它们在并发性能上不如ConcurrentHashMap。

泛型与类型安全

泛型在集合框架中的作用是什么?

泛型在Java集合框架中的作用至关重要,它主要带来了以下几个方面的提升和优势:

  1. 类型安全:泛型允许在编译时检查集合中元素的类型,确保集合只能持有特定类型的数据。这有助于预防类型错误,比如将错误类型的对象添加到集合中,从而避免了运行时的ClassCastException。例如,声明一个List后,尝试添加一个Integer对象将会在编译时期失败。
  2. 消除类型转换:使用泛型后,从集合中取出元素时不需要显式地进行类型转换。因为集合已经知道它包含的是什么类型的对象,所以在获取元素时可以直接使用正确的类型,提高了代码的清晰度和效率。例如,从List中获取元素时,不需要将其转换为String。
  3. 易于阅读和维护:泛型使代码意图更加明确,提高了代码的可读性和自文档化能力。阅读者可以一目了然地知道集合中元素的类型,而不必查看具体的实现代码或者注释。
  4. 通用算法实现:泛型使得编写可以应用于多种数据类型的算法成为可能。例如,你可以编写一个泛型方法来排序任何类型的列表,而无需为每种数据类型单独编写方法。这增加了代码的复用性和灵活性。
  5. 编译时检查:泛型提供了编译时的类型检查,这意味着潜在类型不匹配的错误可以在程序运行前被发现并修正,大大降低了运行时出现问题的可能性,提升了程序的稳定性。

在集合框架中,几乎所有的集合类,如List<T>, Set<T>, Map<K, V>等,都广泛采用了泛型,使得集合操作更加安全、简洁且强大。泛型参数 <T>、<K>、<V> 分别代表类型参数,可以是任何非保留字的标识符,其中 T 通常代表类型(Type),K 代表键(Key),V 代表值(Value)。

解释类型擦除的概念,并谈谈它在使用泛型集合时的影响。

类型擦除是Java泛型实现的一个核心概念。在Java中,泛型是通过编译时的类型检查来提供类型安全,但实际上在编译后(即生成的字节码中),所有的泛型类型信息都会被擦除,替换为它们的非泛型上界(通常是Object,除非指定了其他上界)。这意味着,尽管我们在编写代码时可以指定集合中元素的确切类型,如List<String>,但在编译完成的字节码里,它仅仅表现为一个原始的、未指定类型的List。 类型擦除的影响:

  1. 运行时类型信息丢失:由于类型信息在运行时不存在,所以你不能直接通过反射获取到泛型参数的实际类型。例如,尝试通过Class<List<String>> clazz = List<String>.class这样的代码获取类型信息是不合法的,因为编译器不知道List<String>这样的具体类型信息。
  2. 泛型数组的限制:Java不允许创建泛型数组,因为数组在运行时保留类型信息,而泛型类型信息在运行时被擦除,这会导致类型不安全。例如,List<String>[] stringLists = new List<String>[10];这样的代码会引发编译错误。
  3. 强制类型转换:虽然在使用泛型集合时,编译器帮助我们自动进行了类型转换,但在某些反射或泛型数组的场景下,可能需要手动进行类型转换,增加了出错的风险。
  4. 边界和通配符的影响:类型擦除会影响泛型的边界和通配符的处理。例如,虽然你可以在编译时区分List<Object>和List<? super String>,但在运行时它们被视为相同的类型,即原始的List类。
  5. 静态成员与泛型:由于泛型信息不被保存,静态成员不与泛型参数绑定。这意味着,即使定义了class MyClass<T> { static T myStaticField; },所有实例的myStaticField实际上共享同一个类型,即擦除后的类型,通常为Object。

尽管类型擦除带来了一些限制,但它也有助于保持Java泛型的向后兼容性,使得旧的非泛型代码可以无缝地与新的泛型代码交互,同时不会显著增加运行时的开销。开发者在使用泛型集合时,通常不必直接面对类型擦除的细节,因为编译器会处理大部分相关的转换和检查。

迭代器与遍历

Iterator与ListIterator有什么不同?

Iterator和ListIterator都是Java集合框架中用于遍历集合元素的迭代器,但它们之间存在一些关键性的差异:

  1. 适用范围
  • Iterator是更通用的迭代器,适用于所有实现了Iterable接口的集合,包括List、Set和Map的键集或值集等。
  • ListIterator则是专门为List及其子类设计的,因此它只能用于List类型的集合。
  1. 功能差异
  • Iterator提供了基本的遍历功能,包含hasNext()、next()和remove()方法,允许向前遍历集合并移除元素。
  • ListIterator在Iterator的基础上扩展了功能,不仅可以向前遍历,还支持向后遍历(通过hasPrevious()和previous()方法)。此外,ListIterator允许在迭代过程中修改集合元素(通过set()方法)和在指定位置插入新元素(通过add()方法)。
  1. 遍历方向
  • Iterator只能单向遍历,即从集合的开始向结束遍历。
  • ListIterator则支持双向遍历,用户可以根据需要向前或向后移动迭代器。
  1. 继承关系
  • ListIterator实际上是Iterator的子接口,这意味着任何可以使用Iterator的地方都可以使用ListIterator(尽管实际上由于功能上的特化,不能直接将ListIterator用于非List集合)。

总的来说,当你需要对List集合进行更复杂的操作,如双向遍历或在遍历时修改集合内容,ListIterator是更合适的选择。而对于基本的遍历和删除操作,或者非List集合的遍历,Iterator就足够使用了。

增强for循环与传统的迭代器遍历有何异同?

增强for循环(也称为"foreach"循环)和传统的迭代器(Iterator)遍历都是Java中用来遍历集合或数组元素的方法,但它们在使用方式、灵活性和功能上有所不同:

相同点

  1. 目的相同:两者都是为了简化遍历集合或数组的过程,提供更加便捷的遍历方式。
  2. 内部实现:增强for循环在内部实际上也是调用了迭代器(Iterator)来遍历集合。这意味着对于集合遍历,增强for循环的效率和迭代器相近,都依赖于集合的迭代器实现。
  3. 遍历不可变性:在遍历过程中,两者都不直接支持修改集合内容(虽然可以通过间接方式修改,但这不是推荐的做法,尤其是在增强for循环中这样做可能导致并发修改异常)。

不同点

  1. 语法简洁性:增强for循环的语法更为简洁,直接在循环语句中声明迭代变量,无需显式创建迭代器对象,如 for (Type element : collection) {...},这使得代码更加易读。
  2. 灵活性:迭代器提供了更多的控制,比如使用hasNext()和next()方法可以更精细地控制遍历过程,而且迭代器还提供了remove()方法来在遍历时安全地删除元素。增强for循环则不直接提供这些额外的控制选项。
  3. 适用范围:增强for循环不仅适用于实现了Iterable接口的集合类,也适用于数组;而传统的迭代器遍历主要针对实现了Iterable接口的集合。
  4. 修改集合:如前所述,虽然都不直接支持修改,但通过迭代器遍历时,可以在迭代过程中调用Iterator.remove()方法来安全地删除元素,这一点是增强for循环做不到的,因为直接在增强for循环中修改集合可能会导致ConcurrentModificationException异常。
  5. 获取索引:使用传统的迭代器遍历,并不能直接获取当前元素的索引(除非额外计数),而使用传统的for循环(非增强版)遍历数组或List时可以直接通过索引操作元素。增强for循环同样不直接提供获取元素索引的方式。

总的来说,选择哪种遍历方式取决于具体需求:如果追求代码简洁和易读性,且不需要在遍历时修改集合内容,增强for循环是更好的选择;如果需要更多控制或要在遍历时修改集合,则应使用迭代器遍历。

在遍历集合时修改集合元素会导致什么后果

在遍历集合时直接修改集合的元素(例如,改变元素的属性值而不是结构上的添加或删除),通常不会直接导致ConcurrentModificationException异常。这种修改是允许的,因为这类操作并不改变集合的结构,即元素的个数和顺序没有变化。然而,这种做法可能导致逻辑上的问题,比如遍历过程中看到的元素状态可能与遍历结束时的状态不一致,这取决于具体遍历的时机和修改操作的细节。

但是,如果在遍历过程中试图直接增加或删除集合中的元素,那么情况就会不同。对于大多数标准的集合类(如ArrayList、HashMap的标准迭代器实现),这通常会导致ConcurrentModificationException异常抛出。这是因为迭代器在遍历过程中维护了一个期望的修改计数(expectedModCount),当集合的实际修改计数(modCount)与迭代器的期望计数不匹配时,就会抛出此异常,这是所谓的“快速失败”机制,旨在防止在遍历过程中集合结构发生意外变化。

为了避免此类异常,有以下几种做法:

  • 使用迭代器自身的remove()方法来删除元素,这会同步更新迭代器的期望修改计数。
  • 在遍历前复制集合到一个新的结构中,然后在副本上进行修改。
  • 使用支持并发修改的集合类,如ConcurrentHashMap(对于Map操作)或利用CopyOnWriteArrayList(对于List操作),这些集合专为并发访问设计,能安全地处理并发修改。
  • 分阶段操作,先遍历收集需要修改的信息,之后再基于这些信息进行修改。

总的来说,直接在遍历过程中修改集合元素(特别是结构上的修改)应当谨慎处理,以避免不可预期的行为或异常。

容量与扩容

ArrayList和HashMap等集合默认容量是多少?何时会发生扩容?

在Java中,ArrayList和HashMap的默认容量以及扩容条件如下:

ArrayList:

  • 默认容量是10。这意味着如果你创建一个ArrayList而没有指定容量,它将初始化为能容纳10个元素的容量。
  • 发生扩容的情况:当ArrayList的大小超过了其当前容量,它会自动扩容。扩容的规则通常是将容量增加到原来的1.5倍,加上一定的余量(确保扩容后至少有足够的空间容纳新增的元素)。例如,一个默认初始化的ArrayList在添加第11个元素时,容量会从10扩容到大约16(确切地说是10的1.5倍即15,但因为扩容需要考虑到至少要能容纳下一个即将加入的元素,所以会进行适当的调整以满足这一条件)。

HashMap:

  • 默认容量是16。如果你创建一个HashMap而没有指定容量,它将初始化为能容纳16个键值对的容量。
  • 发生扩容的情况:当HashMap中存储的键值对数量超过了容量乘以加载因子的值时,默认的加载因子是0.75。也就是说,当HashMap中的元素数量超过16 * 0.75 = 12时,就会触发扩容。扩容后的新容量是原容量的两倍。因此,首次扩容时,容量会从16变为32。

请注意,这些默认值和扩容策略是基于Java标准库的一般行为,具体实现可能在不同版本的Java中有所变化。在实际应用中,可以通过构造函数传入初始容量和加载因子来定制这些集合的初始配置,以优化性能和资源使用。

如何控制集合的初始容量以优化性能

控制集合的初始容量是优化Java集合性能的关键策略之一,尤其是在已知或可预估集合大小的情况下。以下是一些有效的方法来优化集合的初始容量:

  • 预估并指定容量:在创建集合时,如果能够预估集合中元素的数量,应直接通过构造函数指定初始容量。例如,对于ArrayList,可以这样做:List<String> list = new ArrayList<>(预计元素数量); 对于HashMap,同样适用:Map<String, Integer> map = new HashMap<>(预计键值对数量); 这样可以减少集合在增长过程中需要动态扩容的次数,从而提升性能。
  • 避免默认容量:不要依赖集合的默认容量(如ArrayList默认的10或HashMap默认的16),特别是当你知道集合将存储大量元素时。直接指定接近实际需要的容量可以避免不必要的扩容操作。
  • 理解扩容机制:理解特定集合类型的扩容规则。例如,ArrayList扩容时会将容量增加到原来的1.5倍,而HashMap在达到加载因子阈值时(默认为0.75)会将容量翻倍。基于这些规则,合理预估以减少扩容频率。
  • 合理设置加载因子:对于HashMap,除了初始容量外,还可以通过构造函数设置加载因子。减小加载因子可以减少冲突,提高查找效率,但会增加内存使用;增大加载因子可以节省内存,但可能降低性能。根据具体情况权衡选择。
  • 利用不可变集合:对于不需要修改的集合,使用不可变集合如ImmutableList(来自Guava库)或Java 9引入的List.of()、Set.of()等,它们不需要考虑扩容问题,且在多线程环境中更安全。
  • 监控和调整:在实际应用中,可以通过性能监控工具发现集合频繁扩容的情况,进而调整初始容量设置,达到最优性能。

通过上述方法,可以在设计和实现阶段就避免因频繁的集合扩容导致的性能损耗,从而提高程序的执行效率。

并发集合

ConcurrentHashMap的工作原理以及相比HashMap的优势。

ConcurrentHashMap的工作原理

  • 分段锁(Segment-based locking)(直至Java 7):在Java 7之前的版本中,ConcurrentHashMap使用了分段锁机制。它将整个映射分为若干个段(Segment),每个段本质上是一个小的哈希表,每个段都有自己的锁。这样,在进行读写操作时,只需要锁定相关的段,而不是整个映射,从而实现了更高的并发性能。定位元素时,首先通过一次哈希运算找到对应的段,然后在这个段内进行第二次哈希运算定位元素。
  • CAS(Compare and Swap)和其他无锁算法(Java 8及以后):从Java 8开始,ConcurrentHashMap摒弃了分段锁的设计,转而采用更细粒度的锁,甚至在某些操作中完全无锁。它使用了Node节点(一种改进的HashEntry)来存储键值对,并利用CAS操作来实现线程安全的元素添加、删除等操作,减少了锁的争用。此外,它还引入了红黑树来优化链表过长的问题,进一步提升了性能。
  • 扩容机制:在扩容时,ConcurrentHashMap采用了“懒惰”扩容的策略,即在插入新元素时检查是否需要扩容,并且扩容过程是逐步完成的,这减少了扩容对整体性能的影响。

相比HashMap的优势

  • 线程安全:ConcurrentHashMap天生为高并发设计,提供了线程安全的读写操作,无需外部同步。而HashMap在多线程环境下不安全,需手动同步或使用Collections.synchronizedMap()包装来确保线程安全。
  • 并发性能:通过分段锁(Java 7及以前)或细粒度锁与无锁算法的结合(Java 8及以后),ConcurrentHashMap能够支持更多的并发写操作,减少了锁的竞争,提高了并发性能。
  • 高级功能:ConcurrentHashMap提供了如forEach、putIfAbsent、replace、computeIfAbsent等高级并发操作,这些操作在多线程环境下表现得更为高效且易于使用。
  • 动态调整:特别是在Java 8及以后的版本中,ConcurrentHashMap的内部结构能够根据负载动态调整,比如链表转换为红黑树,提高了在高散列冲突情况下的性能。

总的来说,ConcurrentHashMap在保证线程安全的同时,通过优化的并发控制机制显著提高了在多线程环境下的执行效率,是处理高并发场景下键值对存储的首选方案。

CopyOnWriteArrayList和CopyOnWriteArraySet适用于哪些场景?

CopyOnWriteArrayList和CopyOnWriteArraySet是Java并发编程中用于处理多线程环境下的集合类,它们特别适用于以下场景:

CopyOnWriteArrayList

  1. 读多写少:由于CopyOnWriteArrayList在写操作时会复制整个底层数组,因此它在写操作比较少而读操作非常频繁的场景下表现**。复制数组的开销在读操作远多于写操作时可以被摊薄。
  2. 迭代期间容许并发修改:在遍历集合的同时,如果有其他线程对集合进行修改,CopyOnWriteArrayList可以保证迭代过程不会抛出ConcurrentModificationException,这对于需要安全遍历而不在乎遍历结果是否反映最新数据的场景很合适。
  3. 事件监听器列表:在如Swing或JavaFX等GUI框架中管理事件监听器列表时,因为添加或删除监听器的操作相对较少,而读取(触发事件时遍历)操作非常频繁,使用CopyOnWriteArrayList可以避免同步开销,同时保证线程安全。
  4. 缓存场景:当缓存的数据不经常变动,但读取非常频繁时,使用CopyOnWriteArrayList或Map的变体可以简化编程模型,提高读取性能。

CopyOnWriteArraySet

  1. 无重复元素的并发集合:当需要一个线程安全的集合来保存不重复的元素,并且读操作远多于添加或删除操作时,CopyOnWriteArraySet是理想选择。它是基于CopyOnWriteArrayList实现的,因此继承了其读取优化的特性。
  2. 配置项管理:在需要维护一个配置项列表,且这些配置项不经常变动但会被多个线程读取时,使用CopyOnWriteArraySet可以确保配置的线程安全性和读取效率。
  3. 任务调度:在任务调度系统中,维护待处理任务列表或已完成任务列表时,如果任务添加或移除的频率远低于任务处理(读取)的频率,CopyOnWriteArraySet可以提供良好的并发支持。

总之,CopyOnWriteArrayList和CopyOnWriteArraySet特别适合那些读取操作远多于修改操作,且可以接受数据视图稍微滞后的并发场景。它们通过牺牲写操作的性能来换取读操作的高效和线程安全。

性能与优化

在性能敏感的应用中,如何选择合适的集合类型?

在性能敏感的应用中,选择合适的集合类型是至关重要的,它直接影响到程序的运行效率。以下是选择集合类型时需要考虑的一些关键因素:

  1. 读写比例
  • 读多写少:如果应用中读操作远远多于写操作,可以考虑使用CopyOnWriteArrayList或CopyOnWriteArraySet,它们在读取时不加锁,适合高并发读取。
  • 写多读少:对于写操作密集的场景,应优先选择支持并发写入的集合,如ConcurrentHashMap。
  1. 线程安全性
  • 如果需要线程安全,可以考虑使用ConcurrentHashMap、CopyOnWriteArrayList或CopyOnWriteArraySet。对于非线程安全的需求,则可以选择ArrayList、HashMap等标准集合,并在必要时手动同步。
  1. 数据结构特性
  • 有序性:如果需要保持插入顺序或自然排序,可以选择LinkedHashMap或TreeMap。对于索引访问和随机访问,ArrayList表现更优。
  • 无序性:对于不需要特定顺序的场景,HashSet或HashMap更为合适。
  • 唯一性:若元素需要唯一,使用HashSet、LinkedHashSet或TreeSet;对于键值对,使用HashMap、LinkedHashMap或TreeMap。
  1. 初始容量与扩容
  • 预估集合大小并指定初始容量,可以减少扩容操作,提高性能。例如,对于ArrayList和HashMap,根据预期的元素数量设定初始容量,避免默认容量导致的频繁扩容。
  1. 内存使用
  • 对于内存敏感的应用,选择更加紧凑的数据结构。例如,如果键值对数量固定且不多,可以考虑使用EnumMap(键为枚举类型)或ArrayMap(Android特有,适用于小型映射)。
  1. 操作类型
  • 频繁增删:LinkedList适合频繁的插入和删除操作,因为其对元素的增加和删除操作更快。
  • 随机访问:如果需要快速随机访问元素,ArrayList因其数组实现而具有优势。
  1. 高级特性
  • 利用集合框架提供的高级接口,如NavigableMap、SortedSet等,根据具体需求选择最合适的实现。

解释ArrayList和LinkedList在插入、删除、查找操作上的性能差异。

ArrayList和LinkedList作为Java中两种常用的集合类型,它们在插入、删除、查找操作上的性能差异主要源于它们底层不同的数据结构:

ArrayList

  • 数据结构:ArrayList底层是一个动态数组,意味着它在内存中是连续存储的。
  • 插入操作:插入元素时,如果在数组末尾添加元素,操作很快,因为只需增加数组的大小并在末尾放置新元素。但如果在中间或开头插入元素,就需要移动该位置之后的所有元素,以腾出空间或保持元素的连续性,这会比较耗时。
  • 删除操作:类似地,删除操作如果发生在数组末尾,只需调整大小即可,速度较快。但在中间或开头删除元素,同样需要移动后续元素填补空位,成本较高。
  • 查找操作:由于ArrayList是连续内存存储,利用索引可以直接访问元素,因此查找操作非常快,时间复杂度为O(1)。

LinkedList

  • 数据结构:LinkedList是一个双向链表,每个元素(节点)包含数据和两个指针,分别指向前一个和后一个节点。
  • 插入操作:在链表中插入元素,无论是头部、中间还是尾部,都只需要改变相邻节点的指针即可,操作时间复杂度为O(1),非常迅速。
  • 删除操作:同样,删除操作也只需要调整相邻节点的指针,时间复杂度也是O(1)。
  • 查找操作:由于链表中的元素不是连续存储的,查找某个元素需要从头节点开始,逐个遍历直到找到目标,时间复杂度为O(n),在数据量大时相对较慢。

总结

  • 查找:ArrayList由于其数组结构,查找效率高,适合查询密集型操作。
  • 插入和删除:LinkedList在插入和删除操作上表现出色,特别是当插入或删除发生在集合的中间时,因为不需要移动其他元素,适合增删操作频繁的场景。

因此,选择哪种集合类型应当基于具体的应用场景和操作需求。如果应用程序需要大量的随机访问元素,而插入和删除操作较少,ArrayList可能是更好的选择。相反,如果插入和删除操作频繁发生,尤其是涉及到集合中间部分的操作,LinkedList则更加高效。

高级话题

如何利用Stream API进行集合操作?给出一些常用操作的例子。

Java 8引入的Stream API提供了一种声明式的数据处理方式,使得对集合的操作更加简洁、高效。以下是利用Stream API进行集合操作的一些常用例子:

创建Stream

  1. 从集合创建
 
  1. 并行Stream
 
  1. 从数组创建
 

过滤(Filter)

筛选年龄大于20的男性用户:

 

映射(Map)

将用户名称转换为大写:

 

排序(Sort)

按年龄升序排序用户:

 

聚合(Reduce)

计算所有用户的年龄总和:

 

统计(Collectors)

统计年龄大于18的用户数:

 

分组(GroupingBy)

按性别分组用户:

 

平坦化(FlatMap)

假设User类中有一个课程列表,平坦化处理得到所有课程名称:

 

匹配(Match)

检查是否有年龄大于20的用户:

 

查找(Find)

查找年龄最大的用户:

 

以上例子展示了如何使用Stream API进行集合的过滤、映射、排序、聚合、统计、分组、平坦化、匹配和查找等操作,极大地简化了集合处理逻辑,提高了代码的可读性和效率。

谈谈你对WeakHashMap的理解及其应用场景。

WeakHashMap是Java集合框架中的一种特殊实现,它与传统的HashMap类似,用于存储键值对(key-value pairs),但其键(key)是弱引用(WeakReference)类型。这意味着当WeakHashMap中的键不再有强引用指向它时,垃圾收集器(GC)可能会回收这些键,进而导致相应的条目从映射中自动移除。这种机制使得WeakHashMap非常适合那些希望自动管理内存、避免内存泄漏的场景,尤其是在缓存实现中。

关键特点

  • 弱键(Weak Keys):与HashMap中键是强引用不同,WeakHashMap中键的引用强度较弱,一旦外部没有强引用指向这些键,垃圾回收器就能回收它们,从而自动从映射中移除对应的条目。
  • 引用队列(Reference Queue):WeakHashMap内部维护一个引用队列(ReferenceQueue),当键被垃圾回收时,其对应的弱引用会被放入这个队列中,WeakHashMap会在执行某些操作(如get、size)之前检查这个队列,并清理已失去引用的条目。
  • 动态清理:由于依赖垃圾回收器的运行时机,WeakHashMap的大小可能在任何时候变化,这使得它不适合那些需要精确控制集合大小的场景。
  • 适用场景限制:由于其键的弱引用特性,WeakHashMap不适用于需要长期稳定存储键值对的场景。

应用场景

  • 缓存实现:在需要自动清理不再使用的缓存项以避免内存泄漏的场景下,WeakHashMap非常有用。例如,它可以用来存储最近使用过的计算结果或数据库查询结果,当内存紧张时,不常访问的缓存项可以被自动回收。
  • 元数据映射:当需要为对象维护额外的元数据,但又不想阻止这些对象被垃圾回收时,可以使用WeakHashMap来存储这些元数据。
  • 监听器和回调注册:在事件驱动的系统中,可以使用WeakHashMap来存储事件监听器或其他类型的回调对象,这样当监听器不再被其他地方引用时,可以自动释放相关资源。

总之,WeakHashMap的设计旨在提供一种自动管理内存的机制,特别适用于构建那些不需要长久保留数据、且希望自动适应内存压力变化的映射结构。

实践问题

给定一个整数列表,如何高效地找出其中的重复元素?

在Java中,找出整数列表中的重复元素可以采用几种不同的策略。以下是几种高效的方法:

  1. 使用HashSet:通过遍历列表并将元素添加到HashSet中,如果添加失败(即add()方法返回false),说明该元素已经存在于集合中,即为重复元素。
  2. 使用HashMap:类似于HashSet,但使用HashMap可以记录每个元素出现的次数,进而找到重复的元素。
  3. 排序后比较:将列表排序后,相邻且相同的元素即为重复元素。这种方法改变了原列表的顺序,并且需要额外的时间复杂度用于排序。
  4. Java 8及以上版本的Stream API:利用Stream API的distinct()和filter()操作可以简洁地找出重复元素。

版权声明


相关文章:

  • java 泛型不支持基础类型2024-10-27 13:26:01
  • java基础笔记062024-10-27 13:26:01
  • java基础加减法2024-10-27 13:26:01
  • java基础代码题随机数之和2024-10-27 13:26:01
  • java基础教程数据库2024-10-27 13:26:01
  • 李兴华java基础笔记2024-10-27 13:26:01
  • java编程基础运算符2024-10-27 13:26:01
  • java基础前十章总结2024-10-27 13:26:01
  • 女生没有基础学java好吗2024-10-27 13:26:01
  • 怎么学好java基础课2024-10-27 13:26:01