主动放弃了华为的offer,别遗憾,来复盘下未招架住的技术拷问
行情动荡不安,看着同组曾经一块并肩战斗过的开发同事们陆续被迫离职,我最近几个月也多少的思维活跃了些,有些惴惴不安,不知何时会轮到自己。想着即使暂时轮不到自己,也要有着被裁后能立马找到下家工作的能力,一直坐以待毙终究沦为案板上的羔羊。
据说华为社招技术岗基本都是OD了,正式的很少很少,但是OD的面试流程及用人标准和正式是一样的,有不少难度,我也想试试,想试下华为大佬的技术盘问下我能抵挡几个回合,于是随机在Boss上选了一个华为OD岗投递了下。
去年8月份,参加过一次华为算法题机考,战败收尾,毕竟我不是985/211名校,及格线要比他们高很多很多;
今年3月份,再次参加了一次,还好,过了;
后来又参加了华为的性格测试、提交了所有的资面材料审核,等着HR帮我推华为招聘的项目组;
4月份工作太忙了,加上五一放假的临近不想给自己太大压力,就和HR讲把推项目组的事情安排到了节后;
后来5月16接到了来自于华为的HR的电话,简单沟通了半小时,随后她替我约了华为的面试官时间,技术一面放在5月18号周六上午10:00,技术二面放在了5月20号周一晚上7:30,完美避开了我的上班时间。两位面试官风格迥异,第一个笑呵呵的比较随和,问技术的同时掺杂着场景题,有来有往有交流,第二个比较高冷严肃,全场拷问技术,一问一答型,回答完后不做任何评价,接着下一题。至于我回答的怎么样,其实我也没底,有些问题确实是我的技术盲区,但我会的也全答上来了,庆幸的是,两位面试官出的现场编程题我全做上来了,每轮都进行了接近一小时左右。
5月21号,HR告诉我两轮技术面通过了,接下来就是用人主管的综面了,综面一般问题都不大,过后就发offer了,从技术面定级的情况下每个月给到 xx K,每年大概是14-16薪的样子,我粗略估算了下,综合年收入能比我现在多10W左右,妈呀,这种破行情下还能让我有这样的好机遇,华为待遇方面果然大气! 可另一个头疼的问题来了,用人部门是其他城市的,开始是想去那个城市发展的,但家里人还是不希望我走太远,我咨询了下可不可以入职后在北京的华为办公,答案是否定的,没辙,我不是一个人,我还有家人在北京上班,不再是那个初次步入社会闯荡的少年了,天南地北皆可去得,只能放弃了这份来之不易的工作,拒绝了综面。 感叹我毕业后来北京已经工作五年,第一份工作呆了2年,第二份工作呆了3年了,我是一个求稳定的人,若不是公司各种资金吃紧,大面积的裁人,换调项目组导致晋升渺茫,停止调薪,我是万不会萌发出去看看的想法,但是人总归给自己个退路么,等有一天裁员这把大刀真的架在了我的脖子上,又当如何?
废话说了那么多,开始回归主题了,我复盘了下在技术面时答得不是很好的几个问题,也接受大家的批评与指正:
1.你知道Spring有哪些发布消息、监听消息的组件么?
初看这问题我以为是想问我消息中间件,我回答了Kafka消息发布监听,异步解耦的知识点,其实不然,这里面试官想考察我对Spring的事件机制了解多少,Spring事件机制名词隐约在哪听过,但工作中却从未用过,算是被难到了,后来面试官也大致给我解释了在Spring中这是个很好用的组件,使用观察者模式啥的,能够降低模块之间的耦合度,像一些发短信,发邮件的功能可以通过这种监听机制自动去处理,和业务代码隔离开,不是通过调用方法的形式。后来我也大致去网上搜了下,大致整理了一些知识点:
(1)概述
Spring的事件机制是Spring框架中的一个重要特性,它基于观察者模式实现,允许应用程序中的组件之间进行松耦合的通信。这种机制允许开发者在应用程序中定义、发布和监听事件,从而实现模块间的解耦,提高代码的可维护性和可扩展性。
(2)核心组件与概念
- 事件(Event):表示应用程序中的某个动作或状态的发生,通常通过继承
ApplicationEvent
类来定义自定义事件。 - 事件源(Event Source):负责发布事件的对象,通常使用
ApplicationEventPublisher
接口的实现类来发布事件。 - 事件监听器(Event Listener):负责监听事件并执行相应操作的对象,需要实现
ApplicationListener
接口。 也可以在方法上添加@EventListener注解实现。 - 事件广播器(Event Broadcaster):在Spring中,这个角色通常由
ApplicationContext
担任,它实现了ApplicationEventPublisher
接口,负责管理事件的发布和监听。
(3)工作流程
- 事件发布:事件源通过
ApplicationEventPublisher
的publishEvent
方法发布事件。 - 事件监听:事件监听器通过实现
ApplicationListener
接口并注册到ApplicationContext
中,从而能够监听到感兴趣的事件。 - 事件处理:当事件发布后,
ApplicationContext
会找到所有订阅了该事件的监听器,并调用它们的onApplicationEvent
方法进行处理。
(4)使用方式
- 定义自定义事件:通过继承
ApplicationEvent
类来定义自定义事件,可以添加与事件相关的数据和方法。 - 创建事件监听器:创建一个实现了
ApplicationListener
接口的类,并指定需要监听的事件类型。在onApplicationEvent
方法中编写事件处理逻辑。 - 发布事件:在需要发布事件的地方,通过
ApplicationEventPublisher
的publishEvent
方法发布自定义事件。
(5)优点
- 解耦:通过事件机制,可以实现模块间的解耦,使代码更加清晰、易于维护。
- 可扩展性:可以方便地添加新的事件和监听器,以满足不同的业务需求。
- 灵活性:事件监听器可以处理多个事件,也可以只处理特定类型的事件。
(6)自己写的简单的Demo代码
// 定义一个事件类 import lombok.Getter; import org.springframework.context.ApplicationEvent; @Getter public class CustomEvent extends ApplicationEvent { private String phone; private String email; public CustomEvent(Object source) { super(source); } public CustomEvent(String phone,String email,Object source){ super(source); this.email = email; this.phone = phone; } } // 定义一个事件发布器 import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Component; @Component public class CustomPublisher implements ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } public void publish(String phone,String email) { CustomEvent event = new CustomEvent(phone,email,"Event Publish"); publisher.publishEvent(event); } } import org.springframework.context.ApplicationListener; /** * / 定义一个事件监听器 - 发短信 */ public class CustomListenerOne implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent customEvent) { System.out.println("I am ListenerOne,I need send message to phone:"+customEvent.getPhone()); } } import org.springframework.context.ApplicationListener; /** * 定义一个事件监听器 发邮件 */ public class CustomListenerTwo implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent customEvent) { System.out.println("I am ListenerTwo,I need send email to "+customEvent.getEmail()); } } import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** * 注解方式实现的事件监听器 */ @Component public class CustomListenerThree { @EventListener public void onApplicationEvent(CustomEvent customEvent) { System.out.println("注解方案:I am ListenerTwo,I need send email to "+customEvent.getEmail()); } @EventListener public void onApplicationEvent2(CustomEvent customEvent) { System.out.println("注解方案:I am ListenerOne,I need send message to phone:"+customEvent.getPhone()); } } import org.springframework.context.annotation.AnnotationConfigApplicationContext; /** 测试启动类,注册各个组件 */ public class MainEvent { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); // 注册事件监听器 context.addApplicationListener(new CustomListenerOne()); context.addApplicationListener(new CustomListenerTwo()); // 注册发布器的bean context.register(CustomPublisher.class); context.refresh(); // 获取发布器并发布事件 CustomPublisher publisher = context.getBean(CustomPublisher.class); publisher.publish("188****865","[email protected]"); context.close(); } }
2.介绍下final、finally、finalize的特性与区别。
final和finally我比较熟悉,当时全都回答上来了,但是finalize我确实之前没有深入了解和使用过,直接和面试官说finalize不清楚了。
- final
final
是一个修饰符,它可以用来修饰类、方法和变量。- 当
final
修饰一个类时,这个类不能被继承。 - 当
final
修饰一个方法时,这个方法不能被重写(在子类中)。 - 当
final
修饰一个变量时,这个变量的值不能被改变(常量)。对于基本类型,值不可变;对于引用类型,引用本身不可变,但引用的对象内部状态可以改变。
- finally
finally
是一个异常处理块的关键字,用于定义在所有情况下都必须执行的代码,无论是否抛出或捕获到异常。- 通常,
finally
块与try
和catch
块一起使用,以确保资源(如文件句柄、网络连接等)在异常发生时也能被正确释放。 - 无论
try
块中的代码是否成功执行,或者catch
块是否捕获到异常,finally
块中的代码都会被执行。
- finalize
finalize
是Object
类的一个方法,它的设计初衷是在垃圾收集器准备释放对象占用的内存之前,被垃圾收集器调用,从而允许对象执行一些清理工作。- 但是,由于
finalize
方法的执行时间是不确定的,并且其执行可能会受到 JVM 实现的影响,因此在现代 Java 编程中,通常不建议依赖finalize
方法进行资源管理或执行其他重要的清理工作。 - 相反,应该使用
try-with-resources
语句(Java 7 引入)或显式的close
方法(如果对象实现了AutoCloseable
或Closeable
接口)来管理资源。
总结:
final
用于声明常量、不可继承的类和不可重写的方法。finally
用于定义无论是否发生异常都必须执行的代码块。finalize
是Object
类的一个方法,用于在对象被垃圾收集之前执行清理工作,但通常不建议使用它。
3.线程的sleep和对象的wait方法都能让线程等待,有什么区别呢?
这个问题我当时回答了一些sleep
方法在指定的时间后自动唤醒,wait
方法只能被其他线程在同一对象上调用 notify
或 notifyAll
方法来唤醒。但是面试官还追问我有没有其他的区别,可见我回答的并不全面,后来也整理了一版,分享给大家:
- 所属类和方法签名:
sleep
是Thread
类的一个静态方法,因此可以在任何线程中调用。其方法签名是public static void sleep(long millis) throws InterruptedException
。wait
是Object
类的一个方法,因此所有对象都可以调用它。其方法签名是public final void wait() throws InterruptedException
(还有其他重载版本,如wait(long timeout)
和wait(long timeout, int nanos)
)。
- 锁机制:
sleep
方法不会释放当前线程持有的任何锁。线程在调用sleep
后会进入 TIMED_WAITING 状态,但会继续持有锁,直到睡眠时间结束。wait
方法会释放当前线程持有的对象锁(即调用wait
方法的对象上的锁)。线程在调用wait
后会进入 WAITING 或 TIMED_WAITING 状态,并且不会持有锁,直到其他线程在同一对象上调用notify
或notifyAll
方法。
- 唤醒机制:
sleep
方法在指定的时间后自动唤醒,或者如果线程被中断,也会提前唤醒。wait
方法只能被其他线程在同一对象上调用notify
或notifyAll
方法来唤醒。如果线程在等待时被中断,它也会提前唤醒,并抛出InterruptedException
。
- 用途:
sleep
通常用于暂停线程的执行一段时间,或者用于实现简单的轮询或延迟。wait
通常用于多线程之间的通信和同步,特别是在实现生产者-消费者模式或其他需要线程间协作的场景时。
- 异常处理:使用注意事项:
- 如果线程在调用
sleep
时被中断,它会清除中断状态(即将中断状态重置为false
)并抛出InterruptedException
。 - 如果线程在调用
wait
时被中断,它也会抛出InterruptedException
,但不会清除中断状态。这意味着调用线程可以立即检查中断状态,并决定是继续等待还是处理中断。
- 如果线程在调用
-
sleep
可以在任何情况下调用,但wait
必须在同步代码块或同步方法中调用,因为它依赖于对象锁。- 在调用
wait
或notify
之前,最好总是先检查对象的锁状态,以避免潜在的并发问题。 wait
和notify
应该与synchronized
关键字一起使用,以确保线程安全。
其余的回答的不好肯定也有,但是记不真切了,怪我没好好背八股文,像一些弱引用强引用的区别记着也没回答好,这没什么分享的价值;Spring AOP底层原理讲了一些没说很细;还有OOM的排查方案我分享了下工作中遇到时怎么排查处理的,但面试官好像想听我讲Linux命令版的排查;其余的也问了一些mysql、spring、并发编程、JVM、Kafka、SpringCould组件、缓存相关、软件设计的一些零零碎碎的知识点,感觉也都回答了七七八八吧。经过机考和两轮技术面,收获还是有的,看来我不能只关注技术的广度,也得静下心来好好看看Java语言本身的一些技术点和原理,还得注意定期复盘,否则曾经掌握过的知识点也会在时间的洪流中被冲刷干净。