遇到过自动拆箱引发的 NPE 问题吗?【⭐⭐⭐⭐】
- 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险
- 三目运算符使用不当会导致诡异的 NPE 异常
String
、 StringBuffer
和 StringBuilder
的区别是什么? String
为什么是不可 变的?
可变性(Mutability):
- String:String是不可变的(Immutable),即一旦创建了String对象,它的值就不能被修改。任何对String对象的操作都会创建一个新的String对象。
- StringBuffer:StringBuffer是可变的(Mutable),它的值可以被修改。可以通过调用其方法来修改StringBuffer对象的值,而不会创建新的对象。
- StringBuilder:StringBuilder也是可变的,与StringBuffer类似,但StringBuilder不是线程安全的。
线程安全性(Thread Safety):
- String:由于String是不可变的,所以是线程安全的。多个线程可以同时访问和使用相同的String对象,而不会出现竞态条件。
- StringBuffer:StringBuffer是线程安全的,它的方法都是同步的(synchronized),因此多个线程可以安全地同时调用StringBuffer对象的方法。
- StringBuilder:StringBuilder不是线程安全的,它的方法不是同步的,因此在多线程环境中使用时需要注意同步问题。
性能(Performance):
- String:由于String是不可变的,每次对String对象进行操作都会创建一个新的String对象,这可能会导致内存消耗和性能问题,尤其是在需要频繁操作字符串时。
- StringBuffer和StringBuilder:由于它们是可变的,不会创建新的对象,因此在需要频繁操作字符串时,它们的性能通常比String好。StringBuilder相比StringBuffer在单线程环境中性能更好,因为它不是线程安全的,不需要同步操作。
总结:
- String是不可变的,线程安全的,但性能可能较低。
- StringBuffer是可变的,线程安全的,适用于多线程环境,但性能可能受同步操作影响。
- StringBuilder是可变的,非线程安全的,适用于单线程环境,性能通常比StringBuffer好。
重写和重载的区别
重写(Override):
- 重写是指子类重新定义(覆盖)了父类中具有相同名称和参数列表的方法。子类可以在重写的方法中提供自己的实现,这样在运行时,调用该方法时将执行子类中的实现而不是父类中的实现。
- 重写要求子类方法的签名(名称和参数列表)必须与父类方法完全相同或者兼容。
- 重写通常用于实现多态,即在运行时根据对象的实际类型来确定调用哪个方法。
重载(Overload):
- 重载是指在同一个类中,可以定义多个方法,它们具有相同的名称但参数列表不同(个数、类型或顺序不同)。
- 重载方法之间的区别主要在于它们的参数列表,编译器根据调用时提供的参数来确定调用哪个重载方法。
- 重载可以提高代码的灵活性和可读性,使得方法名可以根据不同的参数具有不同的行为。
- 重写是针对继承关系中的父类和子类的方法,子类可以覆盖父类的方法提供自己的实现。
- 重载是在同一个类中,针对同一个方法名但是参数列表不同的多个方法,用于提供更灵活的方法调用方式。
==
和equals
的区别
==
运算符比较的是对象的引用地址(内存地址),用于比较两个对象是否指向内存中的同一个对象。equals()
方法比较的是对象的内容,通常被重写以实现特定的内容比较逻辑。
深拷贝和浅拷贝
浅拷贝(Shallow Copy):
- 浅拷贝是指在复制对象时,只复制对象本身,而不复制对象所引用的其他对象。这意味着新对象和原始对象共享相同的引用对象,而不是新建一个引用对象的副本。
- 浅拷贝操作通常只复制对象的基本数据类型成员变量,对于引用类型成员变量,则只是复制了引用地址,两个对象的引用类型成员变量仍指向同一个对象。
深拷贝(Deep Copy):
- 深拷贝是指在复制对象时,不仅复制对象本身,还要复制对象所引用的其他对象。这意味着新对象和原始对象拥有独立的副本,而不是共享引用对象。
- 深拷贝操作会递归地复制对象的所有成员变量,包括基本数据类型和引用类型,确保新对象和原始对象之间没有任何共享的引用对象。
- 浅拷贝只复制对象本身,而不复制对象所引用的其他对象,新对象和原始对象共享相同的引用对象。
- 深拷贝复制对象时会递归地复制对象的所有成员变量,包括基本数据类型和引用类型,确保新对象和原始对象之间没有任何共享的引用对象。
接口和抽象类有什么共同点和区别?如何选择?
共同点:
- 都不能被实例化:接口和抽象类都不能直接实例化,只能通过它们的实现类或子类来实例化对象。
- 都可以包含抽象方法:接口中的方法默认为抽象方法,而抽象类可以包含抽象方法。子类必须实现接口中的所有抽象方法或抽象类中的抽象方法,除非子类自己也声明为抽象类。
都用于实现多态:接口和抽象类都用于实现多态,子类实现接口或继承抽象类后,可以根据需要提供自己的实现,从而实现不同的行为。
区别:
实现方式:
- 接口是一种纯抽象的类型,只能包含抽象方法和常量,不能包含普通方法的具体实现。
- 抽象类是一个可以包含抽象方法和普通方法的类,它可以有构造方法和成员变量。
继承关系:
- 一个类可以实现多个接口(多继承),但只能继承一个抽象类。
- 接口之间可以通过extends关键字来扩展另一个接口,但类只能通过extends关键字来继承一个抽象类。
成员变量:
- 接口中只能包含常量,不能有成员变量,而抽象类可以包含成员变量。
- 接口中的成员变量默认为public static final,而抽象类中的成员变量可以具有各种访问修饰符。
构造方法:
- 接口没有构造方法,而抽象类可以有构造方法。
如何选择:
- 如果需要定义一组行为的规范而不关心具体实现,应该使用接口。
- 如果希望提供一些通用的功能实现,并且允许子类扩展或覆盖部分功能,应该使用抽象类。
- 如果需要实现多继承,或者需要一个类型同时具备多种行为规范,可以考虑使用接口。
如果希望在代码中使用构造方法、成员变量或普通方法的具体实现,可以考虑使用抽象类。
反射&注解&泛型
Java 反射?反射有什么优点/缺点?你是怎么理解反射的(为什么框架需要反射)?
Java反射是指在运行时动态地获取类的信息以及在运行时动态调用类的方法和操作类的属性的能力。Java反射机制提供了一种在运行时检查对象和类的能力,使得程序可以在运行时获取类的信息、构造类的实例、调用类的方法、访问类的属性等。
反射的优点:
- 灵活性和动态性:反射允许程序在运行时动态地加载和操作类,使得程序具有更高的灵活性和动态性,能够根据运行时的条件动态地调整和修改程序的行为。
- 提高代码的通用性和复用性:反射使得编写通用代码和框架变得更加容易,因为它可以在运行时处理未知类型的对象,而不需要事先知道其具体类型。
实现插件化和扩展性:通过反射机制,程序可以在运行时加载和使用外部的类和资源,实现插件化的功能,从而提高了系统的可扩展性和可维护性。
反射的缺点:
- 性能问题:反射操作通常比直接调用方法或访问属性的性能要低,因为它涉及到动态地加载和调用类的信息,需要额外的开销。
- 类型安全问题:反射可以绕过编译时的类型检查,使得代码更容易出现类型安全问题,可能导致运行时的异常。
可读性和维护性问题:反射使得程序更加动态和灵活,但也增加了代码的复杂性和难度,降低了代码的可读性和维护性。
对反射的理解和框架需要反射的原因:
- 理解反射:我认为反射是一种在运行时动态地获取类的信息、调用类的方法和操作类的属性的能力,它使得程序可以更加灵活和动态地加载和操作类,实现更加通用和灵活的代码。
框架需要反射的原因:
- 框架通常需要处理未知类型的对象,因为框架的用户可能会传递不同的类和对象给框架进行处理,而框架本身并不知道具体的类型。
- 反射机制使得框架可以在运行时动态地加载和调用类的方法、访问类的属性,实现更加灵活和通用的功能。
- 例如,Spring框架中的依赖注入、AOP(面向切面编程)等功能就需要使用反射机制来动态地管理和操作类和对象。
谈谈对 Java 注解的理解,解决了什么问题?
Java注解是一种元数据(Metadata)的形式,它们提供了一种在程序代码中添加元数据信息的方法。注解通过在源代码中添加标记,来提供对程序的更多描述信息,从而使得代码能够在编译、运行时或者通过工具进行解析和处理。
- 提供元数据信息:注解允许程序员在源代码中添加元数据信息,用于描述代码的各种属性、行为或者配置。这些元数据信息可以帮助其他程序或者工具更好地理解和处理代码。
- 简化配置和编码:通过注解,可以将一些配置信息直接写在代码中,而不需要额外的配置文件。这样可以减少配置文件的数量和复杂度,使得代码更加简洁、易读和易维护。
- 提高代码的可读性和可维护性:注解使得代码中的一些重要信息更加直观和明确,提高了代码的可读性。同时,通过使用注解,可以使得代码的结构更加清晰,提高了代码的可维护性。
- 简化开发框架和工具:注解为开发框架和工具提供了一种更加灵活和通用的扩展机制。通过注解,开发者可以为自己的框架或者工具提供自定义的配置和行为,从而使得开发框架和工具更加易用和灵活。
- 提供编译期间的检查和验证:某些注解可以在编译期间进行检查和验证,以确保代码的正确性和健壮性。例如,使用注解来标记方法的覆盖、方法的参数检查等,可以在编译时进行检查,避免一些常见的错误。
Java 泛型了解么?泛型的作用?什么是类型擦除?泛型有哪些限制?介绍一下常用的通配 符?
泛型的作用:
泛型是Java语言中的一种特性,它的主要作用是在编译时提供类型安全检查,并且使得代码更加灵活和通用。通过泛型,可以在编写类、接口或方法时指定参数和返回值的类型,从而实现对不同类型的对象进行统一的处理。
类型擦除(Type Erasure):
类型擦除是Java泛型实现的一种机制。在编译过程中,Java编译器会擦除泛型类型的具体信息,将泛型类型替换为其上界类型(对于泛型类或泛型方法)或Object类型(对于泛型接口),以确保与之前的Java版本的兼容性。这意味着在运行时,泛型类型的信息会被擦除,无法在运行时获取到泛型的具体类型信息。
泛型的限制:
- 不能使用基本数据类型实例化泛型类型:例如,不能直接使用int、double等基本数据类型来实例化泛型类,而需要使用对应的包装类Integer、Double等。
- 无法创建泛型数组:不能直接创建泛型数组,例如
List<String>[] lists = new ArrayList<String>[10];
是非法的。 - 无法创建泛型类型的实例:由于类型擦除的存在,无法在运行时直接创建泛型类型的实例,例如
new T()
是非法的。
常用的通配符:
- ? extends T:表示任何实现了T接口的子类,或者T本身。通常用于限制泛型类型的上界,表示泛型类型必须是T或T的子类。
- ? super T:表示任何T的父类,或者T本身。通常用于限制泛型类型的下界,表示泛型类型必须是T或T的父类。
- ?:表示任意类型,相当于通配符。
SPI和API
什么是 SPI?有什么用?
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
SPI(Service Provider Interface)是Java提供的一种服务提供接口,它是一种服务发现机制,用于在运行时动态地扩展或替换框架或应用程序的服务实现。
SPI的主要思想是,定义一个服务接口(Service Interface),并提供一种在运行时发现和加载实现该接口的服务提供者的机制。服务接口定义了服务的抽象行为,而服务提供者则提供了服务的具体实现。通过SPI,可以在运行时动态地发现和加载服务提供者的实现,并在应用程序中使用这些实现。
SPI的主要用途包括:
- 插件化和扩展性:SPI使得应用程序可以通过外部的服务提供者来扩展和增强功能,而不需要修改源代码或重新编译。
- 松耦合:通过SPI,应用程序可以与服务提供者解耦,使得应用程序更加灵活和可维护。
- 动态加载:SPI允许在运行时动态加载服务提供者的实现,从而可以根据需求灵活地选择和加载不同的实现。
- 多态:SPI可以实现多态,同一个接口可以有不同的实现,根据不同的需求选择不同的实现。
SPI的实现方式通常涉及以下几个步骤:
- 定义服务接口:定义一个服务接口,描述服务的抽象行为。
- 实现服务提供者:编写实现服务接口的具体实现类,即服务提供者。
- 配置服务提供者:在META-INF/services目录下创建以服务接口全限定名命名的文件,文件中列出具体的服务提供者的类名。
- 加载服务提供者:通过Java的ServiceLoader类来动态加载服务提供者的实现。
SPI 和 API 有什么区别?
- API是描述程序之间的交互规范,用于定义应用程序的外部接口和使用方法。
- SPI是描述系统的扩展点和扩展方式,用于提供给第三方扩展或替换功能的一组接口或机制。
SPI是使用方也是制定接口的一方,API是调用方制定和实现接口的
Java SPI 实现原理了解吗?
Java SPI的实现原理可以简要描述如下:
- 定义服务接口:首先,需要定义一个服务接口,描述服务的抽象行为。
- 编写服务提供者实现类:实现服务接口的具体实现类,即服务提供者。
- 配置文件:在META-INF/services目录下创建以服务接口全限定名命名的文件,文件中列出具体的服务提供者的类名。
- 服务加载:通过Java的ServiceLoader类来动态加载服务提供者的实现。
在运行时,Java SPI的实现原理可以简单概括为:
- 当调用ServiceLoader.load(Class)方法时,ServiceLoader会通过当前线程的ClassLoader加载指定服务接口的所有服务提供者的实现类。
- ServiceLoader会在META-INF/services目录下查找以服务接口全限定名命名的配置文件,读取配置文件中列出的服务提供者的类名。
- ServiceLoader会使用ClassLoader加载配置文件中列出的服务提供者的类,并实例化它们。
- 将实例化的服务提供者作为迭代器返回,供调用方使用。
IO
I/O 流为什么要分为字节流和字符流呢?
字节流(Byte Stream):
- 字节流以字节(8位)为单位进行数据传输,适用于处理二进制数据(如图片、视频、音频等)或者无格式的文本数据。
- 字节流通常用于处理二进制文件(如图像文件、音频文件、视频文件等)或者与网络、硬盘等设备进行通信时使用,能够直接处理原始字节数据。
字符流(Character Stream):
- 字符流以字符(16位)为单位进行数据传输,适用于处理文本数据,能够直接处理Unicode字符。
- 字符流通常用于处理文本文件(如.txt、.html、.xml等)或者与用户进行交互、读取键盘输入等场景。
分为字节流和字符流的主要原因如下:
- 数据类型的不同:字节流适用于处理二进制数据,字符流适用于处理文本数据,因为文本数据通常是以字符的形式存储的,需要进行字符编码和解码。
- 处理方式的不同:字节流以字节为单位进行读写,适合处理二进制数据和无格式的文本数据,而字符流以字符为单位进行读写,并提供了字符编码和解码的功能,适合处理文本数据。
总结:
在Java中,字节流可以处理任何类型的数据,包括文本数据和字符数据。字节流可以直接处理字节数据,而字符流提供了字符编码和解码的功能,使得处理文本数据更加方便。
Java IO 中的设计模式有哪些?
装饰器模式(Decorator Pattern):
- 装饰器模式是Java IO中最常用的设计模式之一,用于动态地为对象添加额外的功能,而不需要子类化。在Java IO中,InputStream、OutputStream、Reader和Writer都使用了装饰器模式来实现各种输入输出功能的组合。
工厂模式(Factory Pattern):
- 工厂模式在Java IO中被广泛使用,用于创建不同类型的流对象。例如,Java IO中的FileInputStream、FileOutputStream、FileReader和FileWriter等类都使用了工厂模式来创建相应的流对象。
单例模式(Singleton Pattern):
- 单例模式在Java IO中用于保证某些对象的唯一性,例如System.in、System.out、System.err等对象在Java中都是单例对象。
观察者模式(Observer Pattern):
- 观察者模式在Java IO中被用于处理事件监听和回调。例如,Java NIO中的Selector类就是一个典型的观察者模式的实现,用于监听通道的事件。
策略模式(Strategy Pattern):
- 策略模式在Java IO中用于处理不同的输入输出策略。例如,Java NIO中的Buffer类就是一个典型的策略模式的实现,用于定义不同类型的缓冲区。
迭代器模式(Iterator Pattern):
- 迭代器模式在Java IO中用于遍历容器中的数据。例如,Java IO中的File类提供了一些用于遍历文件和目录的方法,这些方法可以看作是迭代器模式的实现。
BIO,NIO,AIO 有什么区别?
BIO(Blocking I/O)、NIO(Non-blocking I/O)和AIO(Asynchronous I/O)是Java中用于处理I/O操作的三种不同的方式,它们有着不同的特点和适用场景。
BIO(Blocking I/O):
- 阻塞式:BIO是一种阻塞式I/O模型,当一个线程在进行I/O操作时,如果数据没有准备好或者无法立即处理,线程将被阻塞,直到数据准备好或者可以处理。
- 同步:BIO是一种同步I/O模型,即在进行I/O操作时,程序会等待操作完成后再继续执行后续的操作。
- 适用性:BIO适用于连接数目较小且固定的场景,例如传统的Socket通信,每个连接对应一个线程。
NIO(Non-blocking I/O):
- 非阻塞式:NIO是一种非阻塞式I/O模型,当一个线程在进行I/O操作时,如果数据没有准备好或者无法立即处理,线程不会被阻塞,而是可以继续执行其他任务。
- 同步和异步混合:NIO是一种同步和异步混合的I/O模型,它提供了Selector、Channel和Buffer等组件,通过Selector监听多个Channel上的事件,当有事件发生时,才进行数据的读取和写入。
- 适用性:NIO适用于连接数目多且连接比较短的场景,例如Web服务器,可以用一个线程处理多个连接,提高服务器的吞吐量和响应速度。
AIO(Asynchronous I/O):
- 异步:AIO是一种异步I/O模型,当一个线程在进行I/O操作时,不需要等待操作完成,而是可以继续执行其他任务,当操作完成后会通过回调的方式通知程序。
- 异步处理:AIO是一种真正的异步I/O模型,数据的读取和写入操作完全交给操作系统来处理,当操作系统完成数据的读取和写入后,会通过回调的方式通知程序。
- 适用性:AIO适用于连接数目多且连接比较长的场景,例如高性能的服务器,可以用少量的线程处理大量的连接,提高服务器的并发能力。
总的来说,BIO适用于连接数目少且固定的场景,NIO适用于连接数目多且连接比较短的场景,而AIO适用于连接数目多且连接比较长的场景。不同的场景可以选择不同的I/O模型来满足需求。