☰
亚信科技Java笔试题深度解析:从出题人视角掌握企业级开发考察逻辑
2026/10/15 1:31:41 网站建设 项目流程

1. 从一道Java笔试题看企业级开发的考察逻辑

聊到“亚信科技——Java笔试题”这个话题,很多准备校招或者跳槽的朋友第一反应就是去网上搜“真题”“面经”,然后对着答案死记硬背。我当年也干过这事儿,后来自己带团队、参与出题之后才发现,这种准备方式效率极低。企业级Java笔试题跟学校考试完全是两码事,它不考你背了多少API,而是通过几道题快速判断你具不具备“上手就能干活”的潜质。亚信科技这类做企业级软件和行业解决方案的公司,业务场景往往涉及高并发、大数据量、复杂业务逻辑,所以他们的笔试题会特别侧重基础扎实度、代码规范意识和问题排查思维。

这篇文章我打算把这类Java笔试题拆开揉碎,从出题人的视角讲清楚每类题到底在考什么、你应该怎么答才能让面试官眼前一亮。不管你是刚学完Java基础的小白,还是工作两三年想进大厂的朋友,这里面的分析思路和实操建议都能直接拿去用。我不会只给你标准答案,而是把每道题背后的“为什么”讲透,再补充一些我实际工作中踩过的坑和总结的技巧。全文会围绕笔试题常见的几大模块展开:基础语法与面向对象、集合框架与数据结构、多线程与并发、JVM与性能调优、数据库与SQL、以及编程题实战。每个模块我都会给出典型题目、解题思路、易错点提醒,以及从企业开发角度延伸出来的经验。

先说一下我对这类笔试题的整体判断。亚信科技的Java笔试题通常不会出现特别偏门的算法,比如让你手写红黑树或者证明NP完全问题,那种题在实际业务里用不到。他们更倾向于考察你对Java核心机制的准确理解,比如String为什么不可变、HashMap的扩容机制、synchronized和ReentrantLock的区别、线程池的参数怎么配、SQL索引失效的场景等等。这些知识点看起来基础,但能准确说清楚的人其实不多。很多候选人能写出“HashMap线程不安全”这个结论,但追问一句“为什么线程不安全、具体在哪个操作上会出问题”就卡壳了。企业要的是能定位问题、能写出健壮代码的人,而不是只会背结论的复读机。

所以我的建议是,准备这类笔试题的时候,不要只盯着答案看,要养成“多问一层”的习惯。每看到一个知识点,就问自己:这个机制解决了什么问题?如果不这样设计会怎样?在什么场景下会出问题?我能不能写一段代码复现这个问题?这种思考方式不仅能帮你应付笔试,更能让你在后续的技术面试和实际工作中脱颖而出。接下来我会按照知识模块逐一展开,每个模块都会给出具体的题目示例和详细解析,同时穿插我在实际项目中的经验教训。

2. 基础语法与面向对象:看似简单,实则暗藏杀机

2.1 String相关题目为什么经久不衰

String是Java笔试题里出现频率最高的类,没有之一。亚信科技的笔试题里经常出现这样的题目:给出几行String创建和拼接的代码,问最终创建了几个对象、输出结果是什么。比如下面这段经典代码:

String s1 = "hello"; String s2 = "hello"; String s3 = new String("hello"); String s4 = "hel" + "lo"; String s5 = s1 + "lo"; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s4); System.out.println(s1 == s5);

这道题考的是字符串常量池、编译期优化和运行期拼接三个知识点。s1和s2都指向常量池里同一个"hello"对象,所以==为true。s3用new关键字强制在堆上创建了新对象,==为false。s4在编译期就被优化成"hello",所以==为true。s5涉及变量参与拼接,编译期无法确定值,会在运行期通过StringBuilder拼接后调用toString()生成新对象,所以==为false。能把这四个结果和原因都说清楚,才算真正理解了String的机制。

但企业开发中更关注的是另一个问题:为什么String要设计成不可变的?这个问题看似简单,但能答全的人很少。我总结下来主要有四点:第一,字符串常量池需要不可变性才能实现复用,如果String可变,一个引用修改了值,其他指向同一对象的引用都会受影响,整个池子就乱了。第二,不可变对象天然线程安全,多线程环境下不需要额外同步。第三,String的hashCode可以缓存,因为值不会变,所以作为HashMap的key时性能更好。第四,安全性,比如网络连接、文件路径这些场景,如果String可变,恶意代码可能在你校验之后偷偷改掉值,造成安全漏洞。这四点里前两点是面试必答的,后两点能说出来就是加分项。

注意:实际开发中做大量字符串拼接时,一定要用StringBuilder而不是直接用+号。我见过一个日志拼接的场景,循环里用+号拼接字符串,数据量上来之后CPU直接飙满,后来改成StringBuilder预分配容量,性能提升了十几倍。这个坑在笔试里不会直接考,但面试官如果问你“平时怎么写字符串拼接”,你主动提出来会显得很有实战经验。

2.2 equals和hashCode的契约关系

另一道高频题是重写equals方法时必须同时重写hashCode,问为什么。这个知识点在《Effective Java》里有详细说明,但很多人只是背了结论。我用一个实际场景来解释:假设你有一个User类,只重写了equals,判断两个User的id相同就认为相等。然后你把两个id相同的User对象分别作为key放进HashMap,你以为第二个put会覆盖第一个,结果发现map里有两个entry。原因就是HashMap先用hashCode定位桶,再用equals比较。如果hashCode没重写,两个对象的hashCode不同,它们会被分配到不同的桶里,equals根本没机会执行。

正确的重写方式是用Objects.hash()或者IDE自动生成,把参与equals判断的字段都纳入hashCode计算。这里有个细节:参与equals的字段必须参与hashCode计算,但参与hashCode计算的字段不一定参与equals,不过为了减少哈希冲突,通常建议保持一致。另外,如果字段可能为null,要用Objects.equals()而不是直接调用.equals(),避免空指针异常。

我在实际项目中遇到过因为hashCode实现不当导致的性能问题。有个同事用所有字段参与hashCode计算,包括一个很长的描述字段,结果HashMap的哈希冲突率极高,查询性能从O(1)退化到接近O(n)。后来改成只用id参与计算,问题立刻解决。所以hashCode的设计原则是:用最少的、最能区分对象的字段来计算,通常就是主键或者业务唯一标识。

2.3 面向对象三大特性的实际应用

笔试里经常出一些代码阅读题,给出一段涉及继承、多态、重载重写的代码,问输出结果。这类题的核心是搞清楚编译期看类型、运行期看对象的规则。比如父类引用指向子类对象,调用一个父类有、子类也有的方法,运行的是子类版本;但如果调用的是父类没有、子类新增的方法,编译就通不过。还有静态方法不存在多态,只看引用类型。

从企业开发的角度看,这些知识点的价值在于帮你写出可扩展的代码。比如策略模式就是多态的典型应用:定义一个接口,不同业务场景写不同的实现类,运行时根据配置动态选择。亚信科技做行业解决方案,不同客户的需求差异很大,这种设计模式用得非常普遍。如果你在笔试里遇到设计题,比如“如何设计一个支持多种支付方式的系统”,能画出接口、实现类、工厂类的结构,并说明为什么这样设计,基本就能拿高分。

3. 集合框架与数据结构:从源码层面理解才能答对

3.1 HashMap的底层原理与扩容机制

HashMap是Java集合里最常考的一个,没有之一。亚信科技的笔试题里几乎每次都会涉及。常见的问题包括:HashMap的底层数据结构是什么?扩容机制是怎样的?为什么容量必须是2的幂?线程不安全体现在哪里?JDK8做了哪些优化?

先说过底层结构。JDK8之前是数组+链表,JDK8之后是数组+链表+红黑树。当链表长度超过8且数组长度大于等于64时,链表会转为红黑树,查询时间复杂度从O(n)降到O(log n)。这个阈值8是怎么来的?其实是基于泊松分布计算出来的,在理想哈希情况下,链表长度达到8的概率不到千万分之一,所以正常情况几乎不会树化,只有哈希函数被恶意构造或者实现不当才会触发。

扩容机制是面试官最爱追问的点。HashMap默认容量16,负载因子0.75,当元素数量超过容量乘以负载因子时触发扩容,每次扩容为原来的两倍。扩容时会把所有元素重新计算索引位置,JDK8对此做了优化:因为容量是2的幂,元素的新位置要么在原位置,要么在原位置加原容量,不需要重新计算hash,只需要看hash值的某一位是0还是1。这个优化非常巧妙,把重新哈希的开销降到了最低。

为什么容量必须是2的幂?因为计算索引时用的是(n - 1) & hash,只有当n是2的幂时,n-1的二进制才是全1,与操作的结果才等价于hash对n取模,但位运算比取模快得多。如果容量不是2的幂,这个等价关系就不成立,索引计算会出错。所以即使你通过构造函数指定了初始容量,HashMap也会把它调整到最近的2的幂。

线程不安全的问题在JDK8之前表现为扩容时可能形成环形链表导致死循环,JDK8之后虽然修复了这个问题,但仍然存在数据覆盖的风险。比如两个线程同时put,都判断到某个位置为空,然后都往那里写,后写的会覆盖先写的。所以多线程环境下必须用ConcurrentHashMap或者Collections.synchronizedMap。

提示:实际开发中如果大概知道元素数量,最好在创建HashMap时就指定初始容量,避免频繁扩容。计算公式是:预期元素数量除以0.75再加1,然后取最近的2的幂。比如预计放100个元素,100/0.75≈134,取128或者256。这个技巧在性能敏感的场景下很有用。

3.2 ArrayList和LinkedList的选择依据

ArrayList和LinkedList的区别是基础题,但很多人只背了“ArrayList查得快、LinkedList增删快”这个结论,没有深入理解背后的原因和适用场景。ArrayList底层是数组,支持随机访问,时间复杂度O(1),但增删元素需要移动后续元素,平均时间复杂度O(n)。LinkedList底层是双向链表,增删只需要修改指针,时间复杂度O(1),但随机访问需要从头或尾遍历,时间复杂度O(n)。

但实际开发中,LinkedList几乎很少用到。为什么?因为即使是在频繁增删的场景,ArrayList的表现往往也不差,甚至更好。原因有几个:第一,ArrayList的内存布局是连续的,CPU缓存命中率高,而LinkedList的节点分散在堆内存各处,缓存不友好。第二,LinkedList每个节点都要额外存储前后指针,内存开销更大。第三,大多数增删操作其实是在列表尾部进行的,ArrayList的尾部增删均摊时间复杂度是O(1)。所以除非你确实需要在列表中间频繁插入删除,并且数据量很大,否则优先用ArrayList。

我在实际项目中做过一个测试,对一个包含10万个元素的列表进行中间插入操作,ArrayList用了大概2秒,LinkedList用了1.5秒,差距并没有想象中那么大。但如果换成遍历操作,ArrayList只用了1毫秒,LinkedList用了800毫秒,差距非常明显。所以选型的时候要综合考虑操作类型和数据规模,不能只看理论复杂度。

3.3 ConcurrentHashMap的线程安全实现

ConcurrentHashMap是并发场景下的首选Map实现,笔试里经常考它的线程安全机制。JDK7用的是分段锁,把整个Map分成多个Segment,每个Segment独立加锁,不同Segment的操作可以并发执行。JDK8之后改成了CAS加synchronized,锁的粒度更细,只锁当前桶的头节点,并发度更高。

具体来说,put操作时如果当前桶为空,用CAS直接写入;如果不为空,用synchronized锁住头节点,然后遍历链表或红黑树进行插入或更新。get操作完全不加锁,因为Node的val和next都用volatile修饰,保证了可见性。size操作比较特殊,因为并发环境下数量一直在变,JDK8用CounterCell数组来分散计数压力,类似LongAdder的思想。

面试官可能会问:ConcurrentHashMap的size()方法返回的是精确值吗?答案是不一定。在并发场景下,size()返回的是一个近似值,因为统计过程中可能有其他线程在增删元素。如果需要精确值,得在无并发操作的时候调用,或者用mappingCount()方法,它返回long类型,精度更高。这个细节能答出来,说明你真的看过源码。

4. 多线程与并发:企业级开发的核心考察点

4.1 线程池的参数配置与拒绝策略

线程池是Java并发编程里最重要的工具,也是亚信科技笔试题的必考内容。常见题目包括:线程池的核心参数有哪些?任务提交后线程池的处理流程是怎样的?拒绝策略有哪几种?如何合理配置线程池大小?

ThreadPoolExecutor的七个核心参数是:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲线程存活时间)、unit(时间单位)、workQueue(工作队列)、threadFactory(线程工厂)、handler(拒绝策略)。任务提交后的流程是:如果当前线程数小于核心线程数,直接创建新线程执行;如果核心线程已满,任务进入队列排队;如果队列已满,且线程数小于最大线程数,创建非核心线程执行;如果队列已满且线程数达到最大,触发拒绝策略。

拒绝策略有四种:AbortPolicy直接抛异常,这是默认策略;CallerRunsPolicy让提交任务的线程自己执行,起到降速作用;DiscardPolicy直接丢弃任务,不抛异常;DiscardOldestPolicy丢弃队列里最老的任务,然后重新提交当前任务。实际开发中,如果任务不能丢,用CallerRunsPolicy比较稳妥;如果可以丢,用DiscardPolicy并记录日志。

线程池大小的配置没有万能公式,要根据任务类型来定。CPU密集型任务,线程数设置为CPU核心数加1,因为CPU密集型任务很少阻塞,线程多了反而增加上下文切换开销。IO密集型任务,线程数可以设置为CPU核心数的两倍甚至更多,因为线程大部分时间在等待IO,多开一些线程能提高CPU利用率。但最准确的方式是通过压测来调整,先给一个经验值,然后观察CPU使用率、队列积压情况和任务平均耗时,逐步优化。

注意:千万不要用Executors工具类创建线程池,虽然方便,但隐藏了风险。newFixedThreadPool和newSingleThreadExecutor用的是无界队列,任务积压可能导致OOM;newCachedThreadPool的最大线程数是Integer.MAX_VALUE,可能创建过多线程导致系统崩溃。阿里开发手册明确禁止使用Executors,必须手动创建ThreadPoolExecutor并指定有界队列。

4.2 synchronized和ReentrantLock的选型对比

synchronized和ReentrantLock的区别是面试高频题。synchronized是JVM层面的关键字,自动加锁释放锁,代码简洁;ReentrantLock是JDK层面的API,需要手动lock和unlock,通常放在try-finally里保证释放。synchronized是非公平锁,ReentrantLock可以选择公平或非公平。synchronized不支持超时和中断,ReentrantLock支持tryLock超时和lockInterruptibly中断。synchronized不支持条件变量,ReentrantLock可以创建多个Condition实现精准唤醒。

实际开发中怎么选?如果只是简单的互斥同步,优先用synchronized,因为代码简洁不容易出错,而且JDK6之后做了大量优化,性能已经和ReentrantLock差不多了。如果需要高级功能,比如超时获取锁、可中断锁、多个条件队列,才用ReentrantLock。我见过一个案例,用synchronized实现生产者消费者模式,因为只有一个等待队列,notifyAll会唤醒所有线程,包括生产者和消费者,造成不必要的上下文切换。后来改成ReentrantLock加两个Condition,分别唤醒生产者和消费者,性能提升明显。

4.3 volatile关键字的两层语义

volatile是Java里最轻量的同步机制,它有两个作用:保证可见性和禁止指令重排序。可见性是指一个线程修改了volatile变量,其他线程立刻能看到最新值。这是通过内存屏障实现的,写操作会在前面插入StoreStore屏障、后面插入StoreLoad屏障,读操作会在前面插入LoadLoad屏障、后面插入LoadStore屏障。禁止重排序是指volatile变量的读写不会和周围的指令交换顺序,这在单例模式的双重检查锁里非常关键。

但volatile不保证原子性,这是最容易出错的地方。比如volatile int count,多个线程同时执行count++,结果仍然可能不对,因为count++是读-改-写三步操作,中间可能被其他线程打断。要保证原子性,得用synchronized或者AtomicInteger。笔试里经常出这种题,给你一段用volatile修饰计数器的代码,问并发执行后结果是否正确,答案是否定的。

双重检查锁的单例实现是volatile的经典应用场景:

public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

这里volatile的作用是禁止instance = new Singleton()这行代码的指令重排序。这行代码实际分三步:分配内存、初始化对象、将引用指向内存地址。如果没有volatile,编译器可能把第二步和第三步重排序,导致其他线程拿到一个还没初始化完成的对象。这个坑非常隐蔽,单线程测试永远发现不了,只有高并发场景才会暴露。

5. JVM与性能调优:拉开差距的关键模块

5.1 内存模型与垃圾回收机制

JVM内存模型是Java笔试的必考内容。运行时数据区包括程序计数器、虚拟机栈、本地方法栈、堆、方法区。程序计数器是唯一不会OOM的区域,虚拟机栈和本地方法栈会抛StackOverflowError,堆和方法区会抛OutOfMemoryError。堆又分为新生代和老年代,新生代又分Eden区和两个Survivor区,默认比例是8:1:1。

对象创建时优先在Eden区分配,当Eden区满时触发Minor GC,存活对象移到Survivor区,年龄加1,达到15岁晋升老年代。大对象直接进老年代,避免在新生代反复复制。老年代满了触发Full GC,通常伴随较长的停顿时间。垃圾回收算法有标记-清除、标记-复制、标记-整理三种,新生代用复制算法,老年代用标记-清除或标记-整理。

垃圾收集器的发展路线是:Serial、ParNew、Parallel Scavenge、CMS、G1、ZGC。CMS以低停顿为目标,用标记-清除算法,会产生内存碎片。G1把堆分成多个Region,可以预测停顿时间,适合大堆场景。ZGC支持TB级堆,停顿时间不超过10毫秒,是未来的趋势。笔试里经常问CMS和G1的区别,以及什么场景下选哪个。我的经验是:堆内存小于4G用Parallel,4G到16G用G1,再大考虑ZGC。当然还要看具体业务对停顿时间的敏感度。

5.2 类加载机制与双亲委派

类加载过程分加载、验证、准备、解析、初始化五步。准备阶段给静态变量分配内存并设零值,初始化阶段才赋真正的初始值。类加载器分启动类加载器、扩展类加载器、应用程序加载器和自定义加载器。双亲委派模型是指加载一个类时,先委托父加载器加载,父加载器加载不了才自己加载。这样保证了核心类库的安全,比如你自己写一个java.lang.String类,永远不会被加载。

但双亲委派是可以打破的。比如JDBC的DriverManager要加载不同厂商的驱动实现,这些实现类在应用程序类路径下,启动类加载器加载不到,所以通过线程上下文类加载器来加载。还有Tomcat为了实现Web应用隔离,每个Web应用有自己的类加载器,优先加载自己目录下的类,加载不到才委托父加载器。这些实际案例能说出来,面试官会觉得你对类加载机制的理解不是停留在背诵层面。

5.3 常用调优工具与实战思路

JVM调优不是靠猜,要靠工具。jps看Java进程,jstat看GC统计,jmap导出堆快照,jstack导出线程快照,jhat分析堆快照。图形化工具推荐JConsole和VisualVM,功能全面且免费。还有Arthas,阿里开源的Java诊断工具,可以在线查看方法调用耗时、反编译类、监控方法出入参,非常强大。

调优的基本思路是:先监控,再分析,后调整。监控看CPU、内存、GC频率和耗时。如果Full GC频繁,先看是不是内存泄漏,用jmap导出堆快照,用MAT分析哪些对象占内存最多。如果确实是内存不够,再考虑加堆内存或者调整新生代老年代比例。如果Minor GC频繁但每次回收很快,可能是新生代太小,适当调大。如果Minor GC耗时很长,可能是存活对象太多,检查是不是有大对象或者生命周期过长的对象。

我踩过的一个坑是:有个服务频繁Full GC,每次停顿好几秒。一开始以为是内存不够,加了堆内存还是不行。后来用jmap分析发现是一个本地缓存没有设过期时间,数据越积越多。改成LRU缓存并设置最大容量后,Full GC频率从每小时几次降到每天一次。所以调优的第一步永远是定位问题,而不是盲目调参数。

6. 数据库与SQL:后端开发的基本功

6.1 索引原理与失效场景

索引是数据库性能优化的核心。B+树是MySQL索引的默认数据结构,特点是所有数据存在叶子节点,叶子节点之间用链表连接,支持范围查询。聚簇索引的叶子节点存整行数据,非聚簇索引的叶子节点存主键值,需要回表查询。覆盖索引是指查询的列都在索引里,不需要回表,性能最好。

索引失效的场景是笔试高频考点。常见的有:在索引列上做运算或函数操作,比如where year(create_time) = 2024;使用不等于号,比如where status != 1;使用is null或is not null,取决于数据分布;like以通配符开头,比如where name like '%张';类型隐式转换,比如字符串列用数字查询;or连接的条件有一个没索引,整个查询可能走全表扫描。这些场景在实际开发中经常遇到,写SQL的时候要特别注意。

提示:用explain命令可以查看SQL的执行计划,重点关注type、key、rows、Extra这几列。type从好到坏依次是system、const、eq_ref、ref、range、index、all,至少要达到range级别。Extra里出现Using filesort或Using temporary说明有排序或临时表,需要优化。这个工具我每次上线新SQL前都会跑一遍,能避免很多性能问题。

6.2 事务隔离级别与MVCC

事务的四个特性ACID是原子性、一致性、隔离性、持久性。隔离级别有四种:读未提交、读已提交、可重复读、串行化。MySQL默认是可重复读,Oracle默认是读已提交。可重复读解决了不可重复读问题,但理论上还存在幻读,不过MySQL通过间隙锁在很大程度上了避免了幻读。

MVCC是多版本并发控制,通过undo log和read view实现。每行数据有两个隐藏列:trx_id记录最后修改的事务ID,roll_pointer指向undo log里的旧版本。读操作时根据read view判断哪个版本可见。读已提交每次查询都生成新的read view,所以能看到其他事务已提交的修改。可重复读只在第一次查询时生成read view,之后一直复用,所以整个事务看到的数据是一致的。

笔试里经常问:可重复读级别下,事务A先查询一次,事务B插入一条新数据并提交,事务A再查询能看到吗?答案是看不到,因为read view没变。但如果事务A执行了update操作,情况就不一样了,update用的是当前读,会看到最新数据。这个细节能答出来,说明你对MVCC的理解很深入。

6.3 SQL优化实战技巧

SQL优化是个系统工程,我总结了几条实用原则。第一,避免select *,只查需要的列,减少网络传输和内存消耗。第二,用join代替子查询,MySQL对join的优化更好。第三,小表驱动大表,减少外层循环次数。第四,批量操作代替循环单条操作,比如用insert into values (),(),()代替多次insert。第五,合理使用limit,避免一次查太多数据。

分页查询是个经典问题。limit 100000, 10这种写法会扫描前100010行然后丢弃前100000行,效率极低。优化方式是用游标分页:where id > 上次最大id order by id limit 10。或者用子查询先定位id:select * from table where id >= (select id from table order by id limit 100000, 1) limit 10。这两种方式都能大幅提升分页性能。

我在实际项目中遇到过一个慢查询,一条统计SQL跑了十几秒。explain发现是全表扫描,因为where条件里对日期字段用了date_format函数。改成范围查询where create_time >= '2024-01-01' and create_time < '2024-02-01'之后,用上了索引,耗时降到几十毫秒。所以写SQL的时候一定要让索引列以裸列的形式出现在条件里,不要包任何函数。

7. 编程题实战:从思路到代码的完整过程

7.1 手写单例模式的几种方式

单例模式是笔试里最常要求手写的设计模式。饿汉式最简单,类加载时就创建实例,线程安全但没有懒加载。懒汉式加synchronized能懒加载但性能差。双重检查锁兼顾懒加载和性能,但要注意volatile。静态内部类方式利用类加载机制保证线程安全,同时实现懒加载,是最优雅的写法。枚举方式最简洁,还能防止反射和反序列化破坏单例,是《Effective Java》推荐的方式。

public enum Singleton { INSTANCE; public void doSomething() { // 业务方法 } }

面试官可能会追问:为什么枚举能防止反射攻击?因为反射创建枚举实例时会抛异常,JDK源码里明确禁止了。为什么能防止反序列化破坏?因为枚举的反序列化不是通过构造函数,而是通过valueOf方法查找已有实例。这些细节能说出来,说明你不仅会写,还理解背后的原理。

7.2 多线程交替打印的实现

多线程交替打印是考察并发编程能力的经典题目。比如两个线程交替打印1到100,一个打印奇数一个打印偶数。实现方式有synchronized加wait/notify、ReentrantLock加Condition、Semaphore、AtomicInteger加自旋等。我用ReentrantLock加Condition写一个示例:

public class AlternatePrint { private static int count = 1; private static final ReentrantLock lock = new ReentrantLock(); private static final Condition oddCondition = lock.newCondition(); private static final Condition evenCondition = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { while (count <= 100) { lock.lock(); try { if (count % 2 == 1) { System.out.println(Thread.currentThread().getName() + ": " + count++); evenCondition.signal(); } else { oddCondition.await(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } }, "奇数线程").start(); new Thread(() -> { while (count <= 100) { lock.lock(); try { if (count % 2 == 0) { System.out.println(Thread.currentThread().getName() + ": " + count++); oddCondition.signal(); } else { evenCondition.await(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } }, "偶数线程").start(); } }

这道题的关键是理解Condition的await和signal机制。await会释放锁并进入等待队列,signal会唤醒等待队列里的一个线程。两个Condition分别管理奇数和偶数线程,实现精准唤醒,避免无效竞争。实际开发中这种模式可以用于生产者消费者、任务调度等场景。

7.3 算法题的高频类型与解题套路

亚信科技的编程题通常不会太难,集中在数组、字符串、链表、二叉树这几个数据结构上。常见题型包括:两数之和、反转链表、判断回文、二叉树遍历、二分查找、快排、归并排序等。准备的时候不要盲目刷题,要按类型总结套路。

数组类题目常用双指针、滑动窗口、前缀和。字符串类常用哈希表、双指针、动态规划。链表类常用虚拟头节点、快慢指针、递归。二叉树类常用递归、层序遍历、深度优先搜索。二分查找要注意边界条件,推荐用左闭右开区间,循环条件是left < right,更新时left = mid + 1或right = mid。

我建议准备笔试的时候,每种类型做5到10道典型题,做完之后自己总结模板。比如二分查找的模板、快排的模板、层序遍历的模板,背下来之后遇到类似题目直接套。但要注意,模板是死的,题目是活的,一定要理解模板为什么这么写,边界条件为什么这么定,否则稍微变一下就不会了。

8. 笔试之外的思考:企业真正想要什么样的人

聊了这么多技术细节,最后说点掏心窝子的话。我参与过几次招聘,也跟做HR的朋友聊过,企业通过笔试筛选人,本质上是在找“基础扎实、思维清晰、有潜力”的候选人。基础扎实意味着你不仅知道结论,还知道原因;思维清晰意味着你能把复杂问题拆解成小问题,一步步解决;有潜力意味着你有学习能力,遇到没见过的问题能自己找到答案。

所以准备笔试的时候,不要只盯着题目本身,要透过题目看本质。每做一道题,问自己三个问题:这道题考的是什么知识点?这个知识点在实际项目中怎么用?如果面试官追问,我还能说出什么?这种准备方式比刷一百道题都管用。另外,平时写代码的时候就要养成好习惯,变量命名规范、异常处理完整、注释清晰,这些细节在笔试的编程题里也会被考察。

还有一点很重要:不要为了笔试而笔试。Java这门语言博大精深,笔试题只是冰山一角。真正让你在职业道路上走得更远的,是对技术的热情和持续学习的能力。我见过很多人笔试成绩很好,但入职之后遇到实际问题就束手无策,因为他们习惯了有标准答案的题目,不习惯没有标准答案的现实问题。所以准备笔试的同时,多动手写项目,多读优秀开源代码,多思考为什么这样设计,这些积累才是你真正的竞争力。

最后分享一个我自己的小习惯:每次做完一道笔试题,不管对错,我都会把解题思路和涉及的知识点整理成笔记,用自己的话写一遍。这个过程能帮我发现哪些地方其实没真正理解,哪些地方是蒙对的。时间长了,笔记越积越厚,知识体系也越来越清晰。这个方法看起来很笨,但效果非常好,推荐你也试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询