《探秘互联网大厂Java面试:核心知识到热门框架全考察》

49 阅读3分钟

在一间明亮的会议室里,一场互联网大厂的Java求职者面试正在紧张进行着。面试官神情严肃,经验丰富,准备对前来面试的程序员进行全方位的考察。而坐在对面的求职者王铁牛,表面上看似镇定,实则心里有些忐忑,他对自己的技术水平心里有数,简单问题尚可应对,复杂一些的就有点没底了。

第一轮面试:

面试官: 首先,我想先了解下一些Java核心知识方面的基础。 第一个问题,Java中基本数据类型有哪些? 第二个问题,说说==和equals的区别吧。 第三个问题,在Java里,静态方法可以重写吗?为什么?

王铁牛: 嗯,Java的基本数据类型有byte、short、int、long、float、double、char、boolean这些。 然后,==比较的是两个对象的地址,equals在没重写之前也是比较地址,不过很多类会重写它来比较内容是否相等。 静态方法不能重写呀,因为静态方法是属于类的,不是属于实例的,子类就算定义了和父类一样签名的静态方法,那也只是在子类自己的范围内重新定义了一个新的静态方法,和重写概念不一样。

面试官:嗯,不错,这些基础掌握得还挺扎实的。

第二轮面试:

面试官: 接下来我们深入一点,关于多线程和线程池这块的知识。 第一个问题,说说创建线程有哪几种方式? 第二个问题,线程池的核心参数有哪些,分别代表什么意思? 第三个问题,在多线程环境下,如何保证共享变量的线程安全?

王铁牛: 创建线程嘛,好像可以继承Thread类,然后重写run方法;还可以实现Runnable接口,把实现类对象传给Thread构造函数;还有就是实现Callable接口,不过这个能返回结果还能抛异常。 线程池的核心参数,嗯……有那个corePoolSize,就是核心线程数吧,还有maximumPoolSize,最大线程数,还有个什么队列来着,哦对,阻塞队列,用来放任务的,其他的不太记得清了。 多线程保证共享变量安全,就用那个synchronized关键字呗,给方法或者代码块加上,就只能一个线程访问了。

面试官:嗯,前面两个回答得还行,不过关于保证共享变量线程安全这块,回答得有点片面了,除了synchronized还有其他方式的,比如使用Lock接口相关的实现类等,不过整体还凑合吧。

第三轮面试:

面试官: 现在我们再聊聊一些常用的框架知识。 第一个问题,简单说下Spring框架的核心模块有哪些? 第二个问题,SpringBoot相比Spring主要有哪些优势? 第三个问题,在使用MyBatis时,#{}和${}的区别是什么?

王铁牛: Spring的核心模块,好像有那个什么Spring Core,用来管理Bean的吧,还有Spring AOP,做面向切面编程的,其他的不太清楚了。 SpringBoot比Spring方便呀,不用配置那么多文件了,启动也快,反正就是用起来简单很多。 MyBatis里的#{}和{},嗯……好像#{}是预编译的,能防止SQL注入,{}就是直接把值拼接到SQL里,不太安全吧。

面试官:嗯,回答得都比较笼统,不过也算是把大概意思说到了一些。行吧,今天的面试就先到这儿吧,你先回去等通知,我们后续会综合评估你的情况再做决定。

面试总结: 今天对王铁牛的面试整体情况来看,在Java核心知识的一些基础部分掌握得还算可以,像基本数据类型以及==和equals的区别等都能准确回答。在多线程和线程池方面,对创建线程的方式有一定了解,但线程池核心参数记得不太准确,保证共享变量线程安全的方式也回答得不够全面。对于常用框架部分,对Spring、SpringBoot和MyBatis的相关知识只是有个大概的认识,回答较为笼统,缺乏更深入细致的理解。还需要综合考量其整体表现以及与岗位的匹配度等来决定是否录用。

以下是问题答案详细解析:

第一轮面试答案解析:

  • Java中基本数据类型
    • Java的基本数据类型分为四类八种。
    • 整数类型:byte(占1个字节,范围是-128到127)、short(占2个字节,范围是-32768到32767)、int(占4个字节,范围是-2147483648到2147483647)、long(占8个字节,范围是-9223372036854775808到9223372036854775807)。
    • 浮点类型:float(占4个字节,单精度浮点数)、double(占8个字节,双精度浮点数)。
    • 字符类型:char(占2个字节,用来表示单个字符)。
    • 布尔类型:boolean(占1位,只有true和false两个值)。
  • ==和equals的区别
    • ==:
      • 对于基本数据类型,比较的是值是否相等。例如:int a = 5; int b = 5; 那么a == b的结果是true。
      • 对于引用数据类型,比较的是对象的引用地址是否相同,也就是是否指向同一个对象。例如:String s1 = new String("hello"); String s2 = new String("hello"); 那么s1 == s2的结果是false,因为它们是两个不同的对象实例,虽然内容相同,但地址不同。
    • equals:
      • 它是Object类中的一个方法,在Object类中,equals方法的默认实现就是比较对象的引用地址,和==对于引用数据类型的比较方式相同。
      • 但是很多类会重写equals方法来实现根据对象的内容来判断是否相等。比如String类就重写了equals方法,当比较两个String对象时,会逐个字符比较它们的内容是否相同,而不是比较地址。所以对于上面的例子,s1.equals(s2)的结果是true。
  • 静态方法可以重写吗?为什么?
    • 静态方法不能重写。
    • 原因是重写是基于对象的多态性概念,是在运行时根据对象的实际类型来决定调用哪个方法。而静态方法是属于类的,不是依赖于对象实例存在的,在编译时就已经确定了调用的是哪个类的静态方法。当子类定义了和父类一样签名的静态方法时,这只是在子类自己的范围内重新定义了一个新的静态方法,它和父类的静态方法没有重写关系,在调用时是通过类名来明确调用哪个类的静态方法,不会出现根据对象类型动态选择调用哪个静态方法的情况。

第二轮面试答案解析:

  • 创建线程的方式
    • 继承Thread类:
      • 创建一个类继承自Thread类,然后重写run方法,在run方法中编写线程要执行的任务逻辑。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行的任务");
    }
}
// 使用时
MyThread myThread = new MyThread();
myThread.start();
    - 注意要调用start方法来启动线程,而不是直接调用run方法,直接调用run方法只是在当前线程中执行了run方法里的任务,并没有真正启动一个新的线程。
- 实现Runnable接口:
    - 创建一个类实现Runnable接口,实现run方法,然后将这个实现类的对象作为参数传递给Thread类的构造函数来创建线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行的任务");
    }
}
// 使用时
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
- 实现Callable接口:
    - 实现Callable接口,需要重写call方法,call方法可以返回结果并且可以抛出异常。与Runnable不同的是,它不能直接作为参数传递给Thread类来创建线程,而是要通过FutureTask类来配合使用。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "线程执行任务的结果";
    }
}

// 使用时
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
// 可以通过futureTask.get()获取call方法返回的结果
String result = futureTask.get();
  • 线程池的核心参数及含义
    • corePoolSize:核心线程数,线程池在创建后会一直保持这些数量的线程处于存活状态,即使它们暂时没有任务可执行,也不会被销毁,除非设置了允许核心线程超时关闭的属性。
    • maximumPoolSize:最大线程数,当任务队列已满,并且正在执行的线程数达到了核心线程数时,如果还有新的任务进来,线程池会创建新的线程来执行任务,但创建的新线程数量不会超过最大线程数。
    • keepAliveTime:当线程池中的线程数量超过核心线程数时,多余的线程在空闲了这个时间长度后会被销毁。
    • unit:是keepAliveTime的时间单位,比如TimeUnit.SECONDS表示秒,TimeUnit.MINUTES表示分钟等。
    • workQueue:工作队列,用于存放等待执行的任务,常见的有ArrayBlockingQueue(基于数组的有界阻塞队列)、LinkedBlockingQueue(基于链表的可选有界或无界阻塞队列)、SynchronousQueue(不存储任务,直接将任务交给线程执行,如果没有空闲线程则阻塞)等。
  • 在多线程环境下,保证共享变量线程安全的方式
    • synchronized关键字:
      • 可以修饰方法,例如:
public synchronized void method() {
    // 这里是对共享变量进行操作的代码
}
    - 这样在同一时刻,只有一个线程能够进入这个方法执行,从而保证了共享变量在方法内的操作是线程安全的。
    - 也可以修饰代码块,例如:
synchronized (this) {
    // 这里是对共享变量进行操作的代码
}
    - 这里的this可以替换成其他对象,只要保证多个线程在访问共享变量时,使用的是同一个锁对象即可。当一个线程进入了这个被synchronized修饰的代码块,其他线程就必须等待这个线程执行完代码块内的操作并释放锁后才能进入。
- Lock接口及其实现类:
    - Lock接口提供了比synchronized更灵活的锁机制。例如ReentrantLock类是Lock接口的一个常用实现类。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SharedResource {
    private int sharedVariable;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            sharedVariable++;
        } finally {
            lock.unlock();
        }
    }
}
    - 首先通过lock.lock()获取锁,然后在try块内对共享变量进行操作,最后在finally块内通过lock.unlock()释放锁,确保无论是否发生异常,锁都能被正确释放。

第三轮面试答案解析:

  • Spring框架的核心模块
    • Spring Core:是Spring框架的基础,主要负责管理Bean的创建、配置、依赖注入等,通过IoC(Inversion of Control)容器实现,让对象的创建和管理更加灵活和方便。
    • Spring AOP:提供面向切面编程的支持,能够将一些横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,以切面的形式进行统一管理和处理,提高代码的可维护性和复用性。
    • Spring Context:构建在Spring Core之上,提供了一种更高级的、基于上下文的方式来管理Bean,包括国际化、事件传播、资源加载等功能。
    • Spring DAO:用于简化数据库访问操作,提供了统一的数据库访问接口和异常处理机制,方便在不同的数据库之间进行切换。
    • Spring ORM:集成了多种流行的对象关系映射(ORM)框架,如Hibernate、MyBatis等,使得在Spring应用中使用ORM框架更加方便。
    • Spring Web:提供了对Web开发的支持,包括创建Web应用程序、处理HTTP请求和响应、实现MVC(Model-View-Controller)架构等。
    • Spring WebMVC:是Spring Web的一部分,专门用于实现MVC架构,将应用程序分为模型、视图和控制器三个部分,提高了Web开发的效率和可维护性。
  • SpringBoot相比Spring主要有哪些优势
    • 简化配置:SpringBoot采用约定优于配置的原则,大量减少了传统Spring应用中需要手动配置的文件和参数,很多默认配置都能满足常见的应用场景,使得开发人员可以更专注于业务逻辑的编写。
    • 快速启动:SpringBoot内置了一个嵌入式的Web服务器(如Tomcat、Jetty等),在启动应用时不需要额外配置和部署Web服务器,大大缩短了启动时间,提高了开发和部署的效率。
    • 自动配置:SpringBoot能够根据项目中引入的依赖自动进行相关配置,例如引入了数据库连接相关的依赖,它会自动配置好数据库连接池等相关设置,减少了开发人员的配置工作量。
    • 方便的依赖管理:SpringBoot通过starter依赖的方式,将相关的一组依赖打包在一起,开发人员只需要引入相应的starter依赖就可以快速搭建起具有特定功能的应用,如引入spring-boot-starter-web就可以快速搭建一个Web应用。
  • 在使用MyBatis时,#{}和${}的区别
    • #{}:
      • 是预编译的处理方式,MyBatis会将SQL语句中的#{}替换成一个问号(?),然后将实际的值通过PreparedStatement的方式进行设置,这样可以有效防止SQL注入攻击。
      • 例如:
SELECT * FROM users WHERE username = #{username}
    - 当传入username的值为'admin'时,实际执行的SQL语句会是:
SELECT * FROM users WHERE username = 'admin'
    - 但在数据库中看到的SQL语句是:
SELECT * FROM users WHERE username =?
- ${}:
    - 是直接将值拼接到SQL语句中的方式,不会进行预编译。
    - 例如:
SELECT * FROM users WHERE username = '${username}'
    - 当传入username的值为'admin'时,实际执行的SQL语句就是:
SELECT * FROM users WHERE username = 'admin'
    - 由于是直接拼接,所以如果传入的值是恶意构造的,就很容易导致SQL注入攻击,因此在使用时要特别小心,一般只在一些特定的场景下,如动态设置表名、列名等情况下使用,并且要确保传入的值是安全可靠的。