10.20 荣耀、知乎、猫眼面试题总结!!!

继续!!!!

猫眼大数据

Java String builder和String buffer区别?

Java中的StringBuilder和StringBuffer都是可变的字符串类,它们之间的主要区别在于线程安全性和性能。

  1. 线程安全性:
  • StringBuilder是非线程安全的,也就是说它的方法没有被同步,不适用于多线程环境。
  • StringBuffer是线程安全的,它的方法都被同步,适用于多线程环境。在多线程环境下,使用StringBuffer可以保证数据的一致性和正确性。
  1. 性能:
  • StringBuilder相对于StringBuffer具有更好的性能,因为StringBuilder的方法没有被同步,不需要额外的同步开销。所以在单线程环境下,使用StringBuilder会比StringBuffer更高效。
  • StringBuffer相对于StringBuilder具有较低的性能,因为它的方法被同步,需要额外的同步开销。所以在多线程环境下,为了保证数据的一致性和正确性,使用StringBuffer是更合适的选择。

综上所述,如果在单线程环境下进行字符串操作,建议使用StringBuilder,而在多线程环境下进行字符串操作,建议使用StringBuffer。

Java String为什么是不可变的?为什么要设计成不可变?

Java中的String是不可变的,这是因为String类被设计成了不可变的对象。这意味着一旦一个String对象被创建,它的值就不能被修改。

Java中设计String为不可变的有以下几个原因:

  1. 线程安全:不可变的特性使得String对象在多线程环境中是安全的。因为它的值不可变,不会被其他线程修改,所以不需要同步控制。
  2. 缓存哈希值:String类将哈希值缓存在对象中,因为它是不可变的,所以哈希值只需要计算一次,而不需要每次使用时重新计算。这样可以提高性能。
  3. 字符串池:Java中的字符串池是为了节省内存而设计的。不可变的String对象可以被共享并重复使用,避免了创建多个相同值的字符串对象。这样可以减少内存占用,提高性能。
  4. 安全性:字符串作为参数传递给一些敏感的API时,不可变的特性可以确保参数的值不会被修改,从而保证数据的安全性。

综上所述,Java中的String被设计成不可变的主要是为了提高性能、确保线程安全以及节省内存。

Java 泛型了解吗?

是的,我了解Java泛型。泛型是Java中一种参数化类型的概念,它允许我们在编写类、接口和方法时使用未知的数据类型。通过使用泛型,我们可以创建通用的代码,可以在不同的数据类型上进行操作,提高代码的复用性和类型安全性。泛型使用尖括号<>来定义,可以用于类、接口和方法的声明和实例化中。在使用泛型时,可以指定具体的数据类型,也可以使用通配符来表示未知的数据类型。

Java反射了解吗?常用的反射方法?

是的,我了解Java反射。

常用的Java反射方法有:

  1. 获取Class对象:通过Class.forName()、对象.getClass()、类名.class等方式获取一个类的Class对象。
  2. 创建实例:通过Class对象的newInstance()方法创建类的实例。
  3. 获取类的成员变量:通过Class对象的getField()、getDeclaredField()等方法获取类的公共或私有成员变量。
  4. 获取类的方法:通过Class对象的getMethod()、getDeclaredMethod()等方法获取类的公共或私有方法。
  5. 调用方法:通过Method对象的invoke()方法调用方法。
  6. 修改成员变量的值:通过Field对象的set()方法修改成员变量的值。
  7. 调用构造函数:通过Class对象的getConstructor()、getDeclaredConstructor()等方法获取类的公共或私有构造函数,并通过Constructor对象的newInstance()方法创建类的实例。

这些方法可以在运行时动态地获取和操作类的信息,使得我们可以在不知道具体类名的情况下,通过反射机制来调用类的方法、访问成员变量等。

Java集合类型?

Java集合类型有以下几种:

  1. List(列表):List是一个有序的集合,可以包含重复元素。常见的实现类有ArrayList和LinkedList。
  2. Set(集合):Set是一个不允许重复元素的集合。常见的实现类有HashSet和TreeSet。
  3. Map(映射):Map是一种键值对的集合,每个键只能对应一个值。常见的实现类有HashMap和TreeMap。
  4. Queue(队列):Queue是一种先进先出(FIFO)的集合。常见的实现类有LinkedList和PriorityQueue。
  5. Stack(栈):Stack是一种后进先出(LIFO)的集合。常见的实现类有Stack。
  6. Vector(向量):Vector是一个动态数组,与ArrayList类似,但是它是线程安全的。

Java数组和链表的区别?

Java数组和链表是两种不同的数据结构,它们在存储和访问数据上有很大的区别。

  1. 存储方式:数组是一种连续的、固定大小的数据结构,它在内存中分配连续的空间来存储元素。链表是一种由节点组成的数据结构,每个节点包含一个数据元素和一个指向下一个节点的指针。
  2. 大小和扩展性:数组的大小在创建时就被确定,并且无法动态扩展。如果需要增加更多的元素,需要创建一个更大的数组并将原来的元素复制到新数组中。而链表的大小可以动态调整,可以根据需要动态添加、删除元素。
  3. 访问速度:数组的访问速度很快,因为可以直接通过索引访问元素。链表的访问速度较慢,需要从头开始遍历链表直到找到目标元素。
  4. 插入和删除操作:数组在插入和删除元素时需要移动其他元素来腾出空间或填补空缺。链表在插入和删除元素时只需要改变指针的指向,不需要移动其他元素,所以插入和删除操作的效率很高。

综上所述,数组适合于需要随机访问元素和已知大小的场景,而链表适合于需要频繁插入和删除元素以及大小不确定的场景。

Java讲讲HashMap原理?转换成红黑树条件?为什么这么设计?

HashMap是Java中常用的一种数据结构,它基于哈希表实现。具体原理如下:

  1. HashMap内部由一个数组和链表(或红黑树)组成。数组是HashMap的主体,用于存储键值对。链表和红黑树用于解决哈希冲突,提高查找效率。
  2. 当添加一个键值对到HashMap中时,首先根据键的hashCode()方法计算出一个哈希值,然后通过哈希值与数组长度取模,得到在数组中的位置。如果该位置上已经存在其他键值对,就发生了哈希冲突。
  3. 如果发生哈希冲突,会在该位置上的链表或红黑树上顺序查找键值对。如果键已经存在,则更新对应的值。如果键不存在,则在链表或红黑树的末尾添加新的键值对。
  4. 当链表长度超过阈值(默认为8)时,链表会转换为红黑树。因为红黑树的查找、插入和删除的时间复杂度都是O(log n),相对于链表的O(n),能够提高查找效率。

转换成红黑树的条件如下:

  • 当链表长度达到8时,且当前数组长度大于等于64,HashMap会将链表转换为红黑树。
  • 当红黑树节点数量小于等于6时,HashMap会将红黑树转换为链表。

这样设计的原因是,当链表长度过长时,查找效率会降低,因为需要遍历链表进行查找。而红黑树相比链表,具有更高效的查找、插入和删除操作,能够更好地提高HashMap的性能。同时,对于较小的链表,转换为红黑树的开销反而比链表更大,所以在节点数量小于等于6时,会将红黑树转换回链表,以节省内存空间。

Java线程安全的HashMap?ConcurrentHashMap和HashTable的区别?ConcurrentHashMap原理?

Java线程安全的HashMap可以使用ConcurrentHashMap来实现。ConcurrentHashMap是Java并发包中的一个线程安全的哈希表实现,它比传统的HashTable和同步的HashMap具有更好的并发性能。

ConcurrentHashMap和HashTable的区别如下:

  1. 锁机制:ConcurrentHashMap使用了分段锁(Segment),每个Segment相当于一个小的HashTable,只锁住当前操作的Segment而不是整个HashTable;而HashTable在每次操作时都锁住整个HashTable。
  2. 并发性:ConcurrentHashMap允许多个线程同时读取,而写操作会锁住相关的Segment,使得在并发写入时性能更好;HashTable在写操作时需要锁住整个HashTable,导致并发写入性能较差。
  3. 扩容机制:ConcurrentHashMap在扩容时只需要锁住相关的Segment,不影响其他Segment的读写操作,提高了并发性能;HashTable在扩容时需要锁住整个HashTable,导致其他线程无法读写,性能较差。

ConcurrentHashMap的原理是基于分段锁(Segment)实现的。它将整个哈希表分成多个小的HashTable(Segment),每个Segment独立地进行锁定和扩容操作。这样可以提高并发性能,允许多个线程同时读取,而写操作只需要锁定相关的Segment,不影响其他Segment的读写操作。同时,ConcurrentHashMap使用了CAS(Compare and Swap)算法来保证并发操作的一致性和线程安全性。

Java进程切换如何保证能够回到之前的执行位置?

Java进程切换是由操作系统负责管理的。当一个Java进程被切换到后台或者切换到其他进程时,操作系统会保存该进程的当前执行状态,包括程序计数器、寄存器和栈等信息。当该进程被重新切换到前台时,操作系统会恢复之前保存的执行状态,使得程序能够回到之前的执行位置继续执行。这种切换和恢复的机制是操作系统提供的,并不由Java语言本身控制。

JVM里面的内存结构?

JVM(Java虚拟机)的内存结构主要分为以下几个部分:

  1. 方法区(Method Area):用于存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等。
  2. 堆(Heap):用于存储对象实例。堆是线程共享的区域,所有线程都可以访问其中的对象。
  3. Java栈(Java Stack):每个线程在创建时都会分配一个对应的Java栈,用于存储局部变量、方法参数、方法返回值以及方法调用的状态。栈是线程私有的,每个线程执行时都拥有自己的栈。
  4. 本地方法栈(Native Method Stack):用于存储本地方法(非Java代码)的调用和执行状态。
  5. 程序计数器(Program Counter Register):存储当前线程执行的字节码指令地址。
  6. 直接内存(Direct Memory):在JVM中,直接内存并不是JVM运行时数据区的一部分,但是它在内存管理中占有重要地位。直接内存是通过操作系统的本地I/O来进行直接分配的,JVM通过本地方法库来操作直接内存。

这些内存区域在JVM的运行过程中扮演不同的角色,用于存储不同类型的数据。在内存管理中,JVM会根据需要动态调整这些内存区域的大小以及内存分配和回收的策略,以提高程序的执行效率和内存利用率。

Java线程池作用?线程池参数?

Java线程池的作用是管理和调度多线程的执行,以提高程序的性能和效率。它通过复用线程对象,避免了线程的频繁创建和销毁,减少了系统开销,并且可以控制同时执行的线程数量,防止系统资源被过度占用。

线程池的参数包括以下几个:

  1. 核心线程数(corePoolSize):线程池中始终保持的线程数量,即使它们处于空闲状态。当有任务提交时,线程池会优先使用核心线程来执行任务。
  2. 最大线程数(maximumPoolSize):线程池中允许的最大线程数量。当核心线程数已满,并且任务队列也已满时,线程池会创建新的线程来执行任务,但数量不能超过最大线程数。
  3. 空闲线程存活时间(keepAliveTime):当线程池中的线程数量大于核心线程数时,多余的空闲线程的存活时间。超过这个时间,空闲线程会被销毁,以减少系统资源的占用。
  4. 任务队列(workQueue):用于存放待执行任务的队列。线程池中的线程会从任务队列中获取任务进行执行。
  5. 拒绝策略(RejectedExecutionHandler):当线程池无法接受新的任务时,采取的处理策略。常见的策略有直接抛出异常、丢弃任务、丢弃队列中最老的任务、使用调用者线程来执行任务。

这些参数的设置需要根据具体的业务场景和系统需求进行合理调整,以达到最佳的性能和资源利用效果。

Hive内部表和外部表区别?

Hive内部表和外部表的区别在于数据的存储和管理方式。

  1. 内部表(Internal Table):内部表是Hive默认创建的表,它的数据存储在Hive的数据仓库中的默认路径下。Hive负责管理内部表的数据和元数据,包括数据的加载、删除、备份等操作。当删除内部表时,Hive会删除该表的元数据和数据。内部表适用于Hive独立管理数据的场景。
  2. 外部表(External Table):外部表是指在Hive中定义的表,但数据存储在Hive之外的位置,例如HDFS上的指定路径或者其他存储系统中。外部表的元数据由Hive负责管理,但数据本身由外部存储系统管理。当删除外部表时,Hive只删除元数据而不删除实际数据。外部表适用于需要与其他系统共享数据的场景,如与其他工具或框架进行数据交互。

总结:

  • 内部表的数据由Hive管理,外部表的数据由外部存储系统管理。
  • 删除内部表会同时删除元数据和数据,而删除外部表只删除元数据。
  • 内部表适用于Hive独立管理数据的场景,而外部表适用于需要与其他系统共享数据的场景。

Hive UDF类别?实现步骤?

Hive UDF(User-Defined Functions)是自定义函数,用于在Hive中进行编程和数据处理。根据功能和使用方式,Hive UDF可以分为以下几类:

  1. 标量函数(Scalar Functions):这类函数接受一个或多个输入参数,并返回单个值作为结果。常见的标量函数包括字符串处理函数(如substr、concat等)、数学函数(如abs、sqrt等)以及日期函数(如year、month等)等。
  2. 聚合函数(Aggregate Functions):这类函数用于对一组值进行计算,并返回一个单一的结果。常见的聚合函数有count、sum、avg、min、max等。
  3. 表生成函数(Table Generating Functions):这类函数接受输入参数,并生成一个新的表作为结果。常见的表生成函数有explode和posexplode等。
  4. 窗口函数(Window Functions):这类函数用于在窗口(由OVER子句定义)上进行计算,并返回一个结果。常见的窗口函数有row_number、rank、dense_rank等。

实现Hive UDF的步骤如下:

  1. 编写Java代码:根据需求,编写Java代码来实现自定义函数的逻辑。需要继承Hive UDF相关的父类(如GenericUDF、GenericUDAFResolver等),并实现相应的方法。
  2. 编译打包:将Java代码编译为可执行的jar文件,并将其添加到Hive的classpath中。
  3. 创建临时函数:在Hive中使用CREATE FUNCTION语句创建临时函数,指定函数的名称、类名称和jar文件路径等信息。
  4. 使用函数:在Hive中使用已创建的自定义函数进行数据处理和计算。可以在SELECT、WHERE、GROUP BY等语句中使用自定义函数。

需要注意的是,自定义函数的实现和使用可能涉及到不同版本的Hive和数据处理引擎,具体的步骤和细节可能会有所差异。

Hive存储文件格式?

Hive支持多种存储文件格式,包括文本文件(如CSV、TSV),序列文件,Avro文件,ORC文件(Optimized Row Columnar),以及Parquet文件。每种文件格式都有其特定的优势和适用场景。

行存储和列存储区别?

行存储和列存储是两种不同的数据存储方式。

行存储是将数据按照行的方式进行存储,即将一条记录的所有字段值存储在一起。这种存储方式适用于需要同时读取一条记录的所有字段值的场景,例如事务型数据库系统。它的优点是数据的写入和更新速度较快,因为只需要修改一条记录的相应字段值。然而,当需要查询特定字段值时,行存储需要读取整条记录,可能会导致性能下降。

列存储是将数据按照列的方式进行存储,即将同一列的数据值存储在一起。这种存储方式适用于需要对特定字段进行聚合操作或者查询特定字段值的场景,例如数据仓库系统。它的优点是在查询特定字段值时具有较高的性能,因为只需要读取该字段所在的列。然而,数据的写入和更新速度会相对较慢,因为需要修改多个列的数据。

总的来说,行存储适用于事务型系统,而列存储适用于分析型系统。选择行存储还是列存储取决于具体的业务需求和查询模式。

举几个开窗函数例子?什么要有开窗函数,和聚集函数区别?

开窗函数是一种在查询结果中进行窗口操作的函数,它可以在查询结果集中的每一行上执行计算,并返回结果集中的一个窗口。以下是几个开窗函数的例子:

  1. ROW_NUMBER(): 返回结果集中每一行的唯一编号。
  2. RANK(): 返回结果集中每一行的排名。
  3. DENSE_RANK(): 返回结果集中每一行的稠密排名。
  4. LAG(): 返回结果集中当前行之前的指定行数的值。
  5. LEAD(): 返回结果集中当前行之后的指定行数的值。
  6. SUM(): 计算结果集中指定列的总和。

开窗函数的作用是对结果集中的每一行进行计算,而不是对整个结果集进行计算。它可以用于实现分组、排序、排名等功能,以及计算每一行与其他行的关联值。

与聚集函数不同,开窗函数不会对结果集进行分组或汇总。聚集函数用于计算整个结果集或每个分组的汇总值,而开窗函数用于在结果集中的每一行上执行计算。

MySQL聚簇索引和非聚簇索引区别?回表操作?

MySQL的聚簇索引和非聚簇索引有以下区别:

  1. 聚簇索引:聚簇索引是按照表的主键或唯一索引来构建的索引结构,它决定了数据在磁盘上的物理存储顺序。每个表只能有一个聚簇索引。聚簇索引的叶子节点存储了完整的行数据,因此可以通过聚簇索引直接访问到所需的数据。
  2. 非聚簇索引:非聚簇索引是按照非主键或非唯一索引来构建的索引结构,它的叶子节点存储了索引列的值和一个指向对应行数据的指针。非聚簇索引的叶子节点并不存储完整的行数据,而是通过指针进行回表操作来获取完整的数据。

回表操作是指当使用非聚簇索引进行查询时,需要通过索引的叶子节点获取到对应的行数据,然后再返回给查询操作。这个过程称为回表操作。由于需要额外的IO操作,回表操作会增加查询的开销。

总结起来,聚簇索引决定了数据在磁盘上的物理存储顺序,可以直接访问数据,而非聚簇索引需要进行回表操作才能获取完整的数据。

HBase读写原理?

HBase是一个分布式、面向列的NoSQL数据库,它基于Hadoop的分布式文件系统(HDFS)存储数据,并使用Hadoop的MapReduce进行计算。下面是HBase的读写原理:

  1. 读取数据:当客户端请求读取数据时,首先需要根据数据的行键(row key)定位到对应的Region Server。在Region Server中,HBase使用MemStore和HFile两个存储结构来存储数据。MemStore是内存中的一个有序数据结构,用于暂时存储最新写入的数据。HFile是磁盘中的存储文件,用于持久化存储数据。当在MemStore中找不到所需的数据时,HBase会去HFile中查找。它通过索引文件(HFile中的Bloom Filter和Block Index)快速定位到所需的数据块,并从磁盘读取数据。一旦找到数据,HBase会将其返回给客户端。
  2. 写入数据:当客户端请求写入数据时,首先需要确定数据所属的Region Server。在Region Server中,数据首先会被写入到MemStore中,这是一个内存缓冲区。当MemStore中的数据达到一定的大小阈值时,或者达到一定的时间阈值时,MemStore中的数据会被刷新到磁盘,生成一个新的HFile。HFile会被存储到HDFS中,并且被分成多个块进行存储。同时,HBase会更新HBase的元数据(如Region的位置、表的结构等)。当需要写入的数据量较大时,HBase还会进行数据预分区,将数据分布到不同的Region中,以实现负载均衡和并行写入。

总结起来,HBase的读写原理是基于HDFS存储数据,使用MemStore和HFile两个存储结构来实现内存和磁盘的数据存储。读取数据时,根据索引文件在内存和磁盘中查找所需的数据块;写入数据时,首先将数据写入到内存中的MemStore,当满足一定条件时,将数据刷新到磁盘生成HFile。同时,HBase还会进行数据预分区和元数据更新。

HBase存储结构?

HBase是一种分布式的、面向列的开源数据库系统,它基于Hadoop的HDFS分布式文件系统进行数据存储。HBase的存储结构可以简单描述为以下几个部分:

  1. 表(Table):HBase中的数据是以表的形式进行存储的,每个表可以被分割成多个区域(Region)。
  2. 行键(Row Key):HBase的表中的每一行都有一个唯一的行键,行键是字节流的形式,用于唯一标识一行数据。
  3. 列族(Column Family):HBase的表中的每一列都属于一个列族,列族是逻辑上的概念,用于对列进行分组。每个列族都有一个唯一的名称。
  4. 列(Column):HBase的表中的数据是以列的形式进行存储的,每个列都有一个唯一的列限定符(Column Qualifier),它与列族名称一起构成列的唯一标识。
  5. 单元格(Cell):HBase的表中的每个单元格都由行键、列族和列限定符组成,它存储着具体的数据值。
  6. 版本(Version):HBase中的每个单元格可以存储多个版本的数据,每个版本都有一个时间戳,用于对数据进行版本控制。

总的来说,HBase的存储结构可以看作是一个多维的稀疏矩阵,通过行键、列族和列限定符来定位具体的数据单元格。这种存储结构使得HBase非常适合存储大规模的结构化、半结构化和非结构化数据。

LSM树原理?

LSM树(Log-Structured Merge Tree)是一种用于存储和管理数据的数据结构,它主要用于解决写放大(write amplification)问题。LSM树主要由两个组件组成:内存中的跳表(memtable)和磁盘中的多层有序磁盘文件(SSTables)。

LSM树的原理如下:

  1. 写操作:当有写操作时,数据首先被写入内存中的跳表(memtable)。跳表是一个有序的数据结构,可以快速插入和查找数据。写入内存的操作是原子的,因此具有较高的写入性能。
  2. 内存满:当内存中的跳表达到一定大小时,会被冻结,并将其转换为一个不可变的有序磁盘文件,称为SSTable(Sorted String Table)。SSTable会被写入磁盘,并在磁盘上保持有序。
  3. 合并操作:当有多个SSTable存在时,会定期触发合并操作。合并操作将多个SSTable合并为一个新的SSTable,并将旧的SSTables标记为删除。合并操作的目的是减少磁盘上的数据冗余,提高查询性能。
  4. 读操作:当有读操作时,首先会在内存中的跳表中查找数据。如果未找到,则会在磁盘上的SSTables中查找。由于SSTables是有序的,可以使用一种称为Bloom Filter的数据结构来快速过滤不存在的数据,从而提高读取性能。

LSM树的优点是具有较高的写入性能和较低的写放大,适用于写多读少的场景,如日志、时间序列数据等。然而,由于合并操作会引入查询时的额外开销,对于读多写少的场景,可能存在一定的查询性能损失。

#晒一晒我的offer##大数据##大数据面试##大数据知识体系##字节跳动#

解决职场真实面试问题,分享同学真实成功案例,欢迎订阅关注!

全部评论

相关推荐

10-15 10:13
已编辑
门头沟学院 Java
记录的意义在于学习,华为的面试官对技术这一块我感觉真的专业,尤其是二面的面试官,感觉很大佬的样子。一面不会的问题:1.&nbsp;元空间相关的垃圾回收问题,full&nbsp;GC2.&nbsp;代理模式和装饰器模式的区别3.&nbsp;final关键字修饰的抽象类能不能被继承4.&nbsp;final关键字修饰的抽象方法能不能被重载和重写5.&nbsp;finalize关键字的作用(其实final是我自己作出来的,真的点到为止即可,不要过多的延伸)6.&nbsp;好像是“遮掩”和“遮蔽”的区别7.&nbsp;项目中提到的使用sharding-jdbc进行分库分表,路由是如何做的8.&nbsp;分库分表分了几个表========================================================二面,面试官看着我的项目问的,用一些开源的项目让如此大佬来问,我真的觉得脸红惭愧,自己做的一堆垃圾,大佬还要想办法如何问这个问题才合适。没答上来的很多,但答案都在项目里写着。1.&nbsp;线程池如何来创建,线程池的核心参数2.&nbsp;如果你写了一个接口,访问的速度非常慢,那你应该如何来处理优化&nbsp;&nbsp;&nbsp;&nbsp;这个问题的原因是项目里写了我用了Redis3.&nbsp;还是这个接口,假设SQL&nbsp;Redis层面都没问题,响应速度还是很慢,你应该如何处理&nbsp;&nbsp;&nbsp;&nbsp;这里我想了一会,他直接提示,从JVM层面考虑,我还是没想到&nbsp;&nbsp;&nbsp;&nbsp;简单一点,你来说一下如何定位问题吧——应该是使用JVM相关的工具,但我一点不会4.&nbsp;相关的设计模式5.&nbsp;看到你用了分库分表,如果有多个实例,你如何避免资源的竞争&nbsp;&nbsp;&nbsp;&nbsp;这个是因为简历上写了Redis的分布式锁,也答了分布式锁SETNX。6.&nbsp;你用到了Kafka,那我问你,Kafka应该在什么场景下使用&nbsp;&nbsp;&nbsp;&nbsp;我把kafka应用场景背了一遍,面试官说是为了连接生产者和消费者(有一个很关键的词,我忘掉了)========================================================主管面,就放开面了,自觉的一二面表现很一般,基本已经知道结果了,就每太纠结,正常和主管聊天了番外:面完晚上还梦到了入职华为,导师是二面的面试官,醒来一看天亮了,才知道什么事真正的白日做梦
投递华为等公司10个岗位
点赞 评论 收藏
分享
3 20 评论
分享
牛客网
牛客企业服务