Java面试题

目录

一、你觉得Java好在哪里?

二、多态是什么意思?

三、Java 中 hashCode 和 equals 方法是什么?它们和==各有什么区别?

四、动态代理是什么?

五、什么是序列化?什么是反序列化?

六、String, StringBuffer, StringBuilder的区别?

七、JDK和JRE的区别

八、注解是什么原理

九、反射用过吗?

十、SPI了解过吗?

十一、泛型有什么用?泛型擦除是什么?

十二、泛型的上下界限定符有了解过吗?

十三、深拷贝和浅拷贝?

十四、Integer缓存池知道吗?

十五、能说下类加载过程吗?

十六、双亲委派知道不?来说说看?

为什么要有双亲委派机制?

那你知道有违反双亲委派的例子吗?

十七、BigDecimal 有了解过吗?

十八、JDK9 把char数组改成了byte数组,你知道为什么吗?

十九、一个线程两次调用 start()方法会出现什么情况?

线程的生命周期

二十、什么是 Optional 类?

二十一、什么是 Java 的网络编程?


一、你觉得Java好在哪里?

1.跨平台

首先Java是跨平台的,不同平台执行的机器码是不一样的,而Java因为加了一层中间层JVM,所以可以做到一次编写多平台运行。

编译执行过程是先把Java源代码编译成字节码,字节码再由JVM解释或JIT编译执行,但JIT需要预热,并且JVM还提供AOT可以直接把字节码转成机器码直接执行。

2.垃圾回收

Java还提供垃圾回收功能,虽说手动管理内容意味着自由、精细化地掌控,但是很容易出错。

在内存较充裕的当下,将内存的管理交给 GC 来做,减轻了程序员编程的负担,提升了开发效率,更加划算!

3.生态

丰富的第三方类库、网上全面的资料、企业级框架、各种中间件等等

二、多态是什么意思?

多态其实是一种抽象行为,它的主要作用是让程序员可以面对抽象编程而不是具体的实现类,这样写出来的代码扩展性会更强。

水果就是抽象,苹果就是具体的实现类。

假设这个人某天开始换口味了,他喜欢吃桃子了,如果我们之前的文章写的是水果,那么完全不需要改,如果写的是苹果,是不是需要把苹果替换成桃子了?

再举个代码的例子比如

Person person = new Student ()

三、Java 中 hashCode 和 equals 方法是什么?它们和==各有什么区别?

hashCode、equals和==都是Java中用于比较对象的三种方式。

  1. hashCode

方法返回对象的哈希码(整数),主要用于支持基于哈希表的集合,用来确定对象的存储位置,如HashMap、HashSet等。

如果两个对象根据equals方法被认为是相等的,那么它们必须具有相同的哈希码。

如果两个对象具有相同的哈希码,它们并不一定相等,但会被放在同一个哈希桶中。

可用于快速比较两个对象是否不同,因为如果它们的哈希码不同,那么它们肯定不相等。

  1. equals

用于比较两个对象的内容是否相等。

通常我们需要在自定义类中重写方法,以基于对象的属性进行内容比较。比如你可以定义两个对象的名字一样就是相等的、年齡一样就是相等,可以灵活按照需求定制。

  1. ==

操作符用于比较两个引用是否指向同一个对象(即比较内存地址),如果是基本数据类型直接比较它们的值。

四、动态代理是什么?

动态代理是 Java 提供的一种强大机制,用于在运行时创建代理类或代理对象,以实现接口的行为,而不需要提前在代码中定义具体的类。

动态是相对于静态来说的,之所以动态就是因为动作发生在运行时。

代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理, 消除一些重复的代码。

动态代理的主要途包括

• 简化代码:通过代理模式,可以减少重复代码,尤其是在横切关注点(如日志记录、事务管理、权限控制等)方面。

• 增强灵活性:动态代理使得代码更具灵活性和可扩展性,因为代理对象是在运行时生成的,可以动态地改变行为。

• 实现 AOP:动态代理是实现面向切面编程AOP的基础,可以在方法调用前后插入额外的逻辑。

五、什么是序列化?什么是反序列化?

序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。

因为对象在JVM中可以认为是“立体”的,会有各种引用,比如在内存地址Ox1234引用了某某对象,那这个对象要传输到网络的另一端的时候就需要把这些引用“压扁”,而另一端的内存地址Ox1234需要将这些扁平的信息再反序列化得到对象。

一个对象可以通过实现Serializable接口来标记它可以被序列化。然后,可以使用ObjectOutputStream来序列化对象,使用ObjectInputStream来反序列化对象。

// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
out.writeObject(myObject);
out.close();

// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
MyObject myObject = (MyObject) in.readObject();
in.close();

六、String, StringBuffer, StringBuilder的区别?

String:适用于少量字符串操作或需要字符串常量池优化的场景。

StringBuffer:适用于多线程下频繁的字符串操作。(线程安全但性能不高)

StringBuilder:适用于单线程环境下频繁的字符串操作。(线程不安全但性能高)

七、JDK和JRE的区别

  1. JRE指的是Java运行环境,包含了JVM、核心类库和其他支持运行Java程序的文件。

JVM:执行Java字节码,提供Java程序的运行环境。

核心类库:如java.lang、java.util等

其他文件:如配置文件、库文件等

  1. JDK可以视为JRE的超集,是用于开发Java程序的完整开发环境,它包含了JRE,以及用于开发、调试和监控java应用程序的工具。

开发工具:如编译器(javac)、调试器(jdb)、打包工具(jar)等

附加库和文件:支持开发、文档生成和其他开发相关的任务。

八、注解是什么原理

注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记身也可以设置一些值。有了标记之后,我们就可以在解析的时候得到这个标记,然后做一些恃别的处理,这就是注解的用处。

比如我们可以定义一些切面,在执行一些方法的时候看下方法上是否有某个注解标记,如果是的话可以执行一些持殊逻辑(RUNTIME类型的注解)。

注解生命周期有三大类,分别是:

RetentionPolicy.SOURCE:给编译器用的,不会写入class文件

RetentionPoIicyCLASS: 会写入class文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了 RetentionPolicy.RUNTIME.:会写入 class 文件永久保存,可通过过反射获取注解信息

九、反射用过吗?

反射其实就是 Java 提供的能在运行期得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。

一般在业务编码中不会用到反射,在框架上用的较多,因为很多场景需要很灵活,不确定目标对象的类型,届时只能通过反射动态获取对象信息。

例如 Spring 使用反射机制来读取和解析配置文件,从而实现依赖注入和面向编程等功能。

例如动态代理场景可以使用反射机制在运行时动态地创建代理对象。

反射机制的优点是:

• 可以动态地获取类的信息,不需要在编译时就知道类的信息。

• 可以动态地创建对象,不需要在编译时就知道对象的类型。

• 可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。

反射机制的缺点是:性能问题

• 在高并发场景下,反射在运行时进行,所以程序每次反射解析检查方法的类型等都需要从 class 的类信息加载进行运行时的动态检查。所以 Apache BeanUtils 的 copy 在高并发下就有性能问题。

如何优化:缓存

例如第一次得到 Method 缓存起来,后续就不需要再调用 Class.getDeclaredMethod 也就不需要再次动态加载了,这样就可以避免反射性能问题。

十、SPI了解过吗?

SPI(Service Provider interface)服务提供接口是 Java 的机制,主要用于实现模块化开发和插件化扩展。SPI机制允许服务提供者通过特定的配置文件将自已的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。

例如一个典型的 SPI 应用场景是 JDBC ,不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。

例如主流 Java 开发框架中,Servlet容器、日志框架、Spring框架等都使用到了 SPI 机制。

如何实现:系统实现和自定义实现

• 系统实现:首先在 resources 资源目录下创建 META-INF/services 目录,并且创建一个名称为要实现的接口的空文件,在文件中填写自己定制的接口实现类的完整类路径,如图

直接使用系统内置的 ServiceLoader 动态加载指定接口的实现类,代码如下:

// 指定序列化器
Serializer serializer = null;
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(serializer.class);
for(Serializer service : serviceLoader){
    serializer = service;
}

上述代码能够获取到所有文件中编写的实现类对象。

• 自定义 SPI 实现:可以定制多个不同的接口实现类。

比如读取如下配置文件,能够得到一个序列化器名称=>序列化器实现类对象的映射,之后就可以根据用户配置的序列化器名称动态加载指定实现类对象。

jdk=com.yupi.serializer.JdkSerializer
hessian=com.yupi.serializer.HessianSerializer
json=com.yupi.serializer.JsonSerializer
kryo=com.yupi.serializer.KryoSerializer

十一、泛型有什么用?泛型擦除是什么?

泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转);并且在编译期能识别类型。

泛型擦除指的是参数类型其实在编译之后就被抹去了。也就是生成的class文件是没有泛型信息的,所以称之为擦除。

泛型擦除例如:

public class Yes<T>{
    private Yes<String> yess;
    private T y;
}

代码编译后的 class 文件:

可以看到编译后的 class 文件中 yess 是泛型,y是Object;这也解释了为什么根据反射是可以拿到泛型信息。

十二、泛型的上下界限定符有了解过吗?

上界限定符是 extends,下界限定符是 super

// 定义一个泛型方法,接收任何继承自Number的类型
public <T extends Number> void processNumber(T number){
    // 在这个方法中,可以安全地调用Number的方法
    double value = number.doubleValue();
}

代码中:<? extends T>表示类型的上界,?这个类型要么是T,要么是T的子类

// 定义一个泛型方法,接收任何类型的List,并向其中添加元素
public <T> void addElements(List<? super T> list,T elment){
    list.add(element);
}

代码中:<? super T>表示类型的下届(也叫做超类型限定),?这个类型是T的超类型(父类型),直至Object

在使用上下界通配符时需要遵守上界生产,下界消费(pecs),即要从集合中读取类型T的数据,并且不能写入,可以使用 ? extens 通配符;要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super通配符,我们是要往方法(add)中传入T类型,也就是方法帮我们消费。

十三、深拷贝和浅拷贝?

深拷贝:完全拷贝一个对象,包括基本类型和引用类型,,堆内的引用对象也会复制一份。

浅拷贝:仅拷贝进本类型和引用,堆内的引用对象和被拷贝的对象共享。

所以假如拷贝的对象成员间有一个 list, 深拷贝之后内有 2 个 list, 之间不会影响,而浅拷贝的话堆内还是只有一个 list。

因此深拷贝是安全的,浅拷贝的话如果有引用对象则原先和拷贝对象修改引用对象的值会相互影响。

十四、Integer缓存池知道吗?

因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 lnteger 搞了个缓存池,默认范围是-128 到 127 ,可以通过设置 JVM-XX:AutoBoxCacheMax=<size> 来修改缓存的最大值,最小值改不了。

实现的原理是int在自动装箱的时候会调用Integer.valueOf,进而用到了IntegerCache。

IntegerCache在静态块中会初始化好缓存值。

当Integer数值在127之内则是同一个对象所以相等。

不仅Integer有,Long也是有的,不过范围是写死的-128到127;但Float和Double是没有缓存池的,毕竟是小数,能存的数太多了。

十五、能说下类加载过程吗?

类加载顾名思义就是类加载到JVM中,而输入一段二进制流到内存,之后经过一番解析、处理转化成可用的class类,这就是类加载要做的事情。

类加载流程分为加载、连接、初始化三个阶段,连接还能拆分为:验证、准备、解析三个阶段。

所以总的来看可以分为5个阶段:

• 加载:将二进制流搞到内存中来,生成一个 Class 类。

• 验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前JVM版本等之类的验证。

• 准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。

• 解析:将常量池的符号引用转化成直接引用。符号引用可以理解为只是个替代的标签。

• 初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

十六、双亲委派知道不?来说说看?

一种使用类加载器的方式。如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。

在 JDK9 之前,Java 自身提供了3种类加载器:

  1. 启动类加载器,它是属于虚拟机自身的一部分,用C++实现的,主要负责加载<JAVA_HOME\lib>目录中或被 -Xbootclasspath 指定的路径中的并且文件名是被虚拟机识别的文件。它是所有类加载器的父亲。
  2. 扩展类加载器,它是 Java 实现的,独立于虚拟机,主要负责加载<JAVA_HOME\lib\ext目录中或被java.ext.dirs系统变量所指定的路径和类库。
  3. 应用程序类加载器,它是 Java 实现的,独立于虚拟机。主要负责加载用户类路径上的类库,如果我们没有实现自定义的类加载器那这玩意就是我们程序中的默认加载器。

一般情况类加载会从应用程序类加载器委托给扩展类再委托给启动类,启动类找不到然后扩展类找,扩展类加载器找不到再应用程序类加载器找。

为什么要有双亲委派机制?

它使得类有了层次的划分(安全性)。就拿 java.lang.Object 来说,加载它经过一层层委托最终是由 BootstrapClassLoader来加载的,也就是最终都是由 BootstrapClassLoader 去找 \lib 中 rt.jar 里面的 java.lang.Object 加载到 JVM 中。

这样如果有不法分子自己造了个 java.lang.Object,里面嵌了不好的代码,如果我们是按照双亲委派模式来实现的话,最终加载到 JVM 中的只会是我们 rt.jar 里面的东西,也就是这些核心的基础类代码得到了保护。

因为这个机制使得系统中只会出现一个 java.lang.Object。

那你知道有违反双亲委派的例子吗?

典型的例子就是:JDBC。

JDBC 的接口是类库定义的,但实现是在各大数据库厂商提供的 jar 包中,那通过启动类加载器是找不到这个实现类的(jar包中没有),所以就需要应用程序加载器去完成这个任务,这就违反了自下而上的委托机制。

如何解决:

具体做法是搞个线程上下文类加载器,通过 setContextClassLoader()默认设置应用程序类加载器,然后通过Thead.current.currentThread().getContextClassLoader()获得类加载器来加载。

十七、BigDecimal 有了解过吗?

BigDecimal 是 Java 中提供的一个用于高精度计算的类,属于 java.math 包。

主要特点:

• 高精度:BigDecimal 可以处理任意精度的数值,而不像 float 和 double 存在精度限制。

• 不可变性: BigDecimal 是不可变类,所有的算术运算都会返回新的 BigDecimal 对象,而不会修改原有对象(所以要注意性能问题)。

• 丰富的功能.提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。

十八、JDK9 把char数组改成了byte数组,你知道为什么吗?

为了节省内存空间,提高内存利用率。

在 JDK 9 之前, String 类是基于 char[ ] 实现的,内部采 UTF-16 编码,每个字符占用两个字节。

但是,如果当前的字符仅需一个字节的空间,这就造成了浪费。例如一些 Latin-1 字符用一个字节即可表示。因此 JDK9 做了优化采用 byte 数组来实现.

十九、一个线程两次调用 start()方法会出现什么情况?

会报错!因为在 Java 中,一个线程只能被启动一次!所以尝试第二次调 start() 方法时,会抛出IIIegaIThreadStateException 异常。

这是因为一旦线程开始执行,它的状态不能再回到初始状态。线程的生命周期不允许它从终止状态回到可运行状态。

线程的生命周期

在 Java 中,线程的生命周期主要包括以下几个状态:

• 新建 (New) :当一个线程对象被创建时,如Thread t = new Thread( );

• 就绪 (Runnable) :当调用 start() 方法时,线程进入就绪状态等待 CPIJ 调度。

• 运行 (Running):线程被调度并执行 run() 方法的内容。

• 阻塞(Blocked ):线程因为某些原因(如等待资源、锁等)进入阻塞状态。

• 终结 (Terminated) :线程执行完 run() 方法或因异常退出。

二十、什么是 Optional 类?

Optional 是 Java 8 引入的一个容器类,它来表示一个值可能存在或不存在。

常见的使用方式如下:

Optional<User> userOption = Optional.ofNullable(userService.getUser(...));
if(!userOption.isPresent()){...}

Optional 可以给返回结果提供了一个表示无结果的值,而不是返回 null。如果用了 Optional,代码里不需要判空的操作,即使 address、province为空的话,也不会产生空指针错误,这就是Optional带来的好处!

二十一、什么是 Java 的网络编程?

Java 的网络编程主要利用 java.net 包,它提供了用于网络通信的基本类和接口。

Java 网络编程的基本概念:

• IP 地址:用于标识网络中的计算机。

• 端口号:用于标识计算机上的具体应用程序或进程。

• Socket(套接字):网络通信的基本单位,通过 IP 地址和端口号标识。

• 协议:网络通信的规则,如 TCP(传输控制协议)和 UDP (用户数据报协议)。

Java 网络编程的核心类:

• Socket:用于创建客户端套接字。

• ServerSocket:用于创建支持 UDP 协议的套接字。

• URL:用于处理统一资源定位符。

• URLConnection:用于读取和写入 URL 引入的资源。

  • 36
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值