面试总结-Java基础
抽象类与接口的区别?
抽象类(Abstract Class)和接口(Interface)是面向对象编程中常见的两种抽象类型,它们在一些方面相似,但在设计和用法上有一些区别:
-
定义:
- 抽象类:是一种包含抽象方法(即没有具体实现的方法)的类,不能被实例化,通常用于作为其他类的父类,子类需要实现其中的抽象方法才能被实例化。
- 接口:是一种完全抽象的类,其中只包含抽象方法和常量字段,不能包含普通方法的具体实现。接口定义了一组行为规范,而实现该接口的类则必须提供这些行为的具体实现。
-
继承:
- 抽象类:允许包含普通方法和字段,可以有构造函数,允许单继承。
- 接口:不能包含字段,方法都是抽象的,不允许包含构造函数,一个类可以实现多个接口。
-
用途:
- 抽象类:适用于在多个相关的类之间共享代码和行为,提供一种基础结构,子类可以通过继承并实现其中的抽象方法来扩展其功能。
- 接口:用于定义类之间的契约,实现了相同接口的类保证具有相似的行为,但可以完全不同的实现。接口提供了一种解耦的方式,使得代码更加灵活。
-
默认实现:
- 抽象类:可以包含具体的方法实现,子类可以选择性地覆盖这些方法。
- 接口:在之前的Java版本中不支持具体方法的实现,但从Java 8开始,接口可以包含默认方法和静态方法的实现,但默认方法不是强制要求实现的。
Java 中深拷贝与浅拷贝的区别?
-
浅拷贝(Shallow Copy) :
浅拷贝会创建一个新对象,但对象的内部引用仍然指向原始对象的内部引用。
- 对于基本数据类型,会复制值,但对于引用类型,只会复制引用,不会复制引用指向的对象本身。
- 因此,当修改新对象的内部引用时,原始对象中对应的内部引用也会受到影响,因为它们指向同一个对象。
-
深拷贝(Deep Copy) :
深拷贝会创建一个新对象,并且递归地复制对象的所有内部引用,包括内部引用指向的对象本身。
- 不论是基本数据类型还是引用类型,都会被完全复制,这意味着新对象和原始对象的内部引用指向不同的对象实例。
- 由于深拷贝会复制对象的所有内容,因此可能会消耗更多的系统资源和时间。
谈谈Error和Exception的区别?
在Java中,Error
和Exception
都是Throwable
类的子类
Error:
Error
表示严重的问题,通常是由于系统级别的错误或者虚拟机出现无法恢复的问题而引起的。Error
一般不应该被程序捕获和处理,因为它们通常表示了程序无法处理的情况,例如内存溢出(OutOfMemoryError
)、虚拟机崩溃(VirtualMachineError
)等。
Exception:
Exception
表示在程序运行期间可能会遇到的可处理的异常情况。- 程序可以通过捕获和处理
Exception
来恢复或者执行适当的操作,例如使用try-catch
块处理异常,或者通过throws
子句将异常传播到调用者。
什么是反射机制?反射机制的应用场景有哪些?
反射机制(Reflection)是一种在运行时动态地获取类的信息以及操作类的属性、方法、构造函数等的机制。在Java中,反射机制允许程序在运行时检查和修改类、对象和方法的结构、行为和属性。通过反射,程序可以动态地创建对象、调用方法、访问属性,而无需提前知道类的结构。
反射机制的主要功能包括:
- 获取类的信息:可以通过反射获取类的信息,如类名、父类、接口、构造函数、字段、方法等。
- 动态创建对象:可以通过反射动态地创建类的实例,即使在编译时无法确定具体类的类型。
- 动态调用方法:可以通过反射动态地调用对象的方法,包括公有方法、私有方法和静态方法。
- 访问和修改属性:可以通过反射访问和修改对象的字段,包括公有字段和私有字段。
- 处理注解:可以通过反射获取类、方法和字段上的注解信息,并根据注解信息进行相应的处理。
应用场景:
- 框架和库的设计:许多框架和库,如Spring、Hibernate等,都广泛使用了反射机制,以实现灵活、可扩展的功能。
- 动态配置:通过读取配置文件中的类名和方法名,使用反射机制动态加载和执行类和方法。
- 插件系统:允许用户编写自定义的插件,并在运行时动态加载和执行这些插件。
- 序列化和反序列化:反射机制可以用于实现对象的序列化和反序列化,即将对象转换为字节流或者将字节流转换为对象。
- 单元测试:在单元测试中,可以使用反射机制访问和测试私有方法和字段,以确保代码的完整性和正确性。
- 工具和调试:一些开发工具和调试工具,如IDE、调试器等,都使用了反射机制来分析和操作程序的结构和行为。
谈一谈你对Java泛型中类型擦除的理解,并说说其局限性?
Java 泛型中的类型擦除是指在编译时期,泛型类型信息会被擦除,转换为原始类型,以确保 Java 的向后兼容性。这意味着在运行时,泛型类或方法的参数类型和返回类型都会变成原始类型,即没有指定泛型类型的情况下。这样做是为了保证 Java 的泛型实现与早期版本的代码和库的兼容性。
具体来说,Java 泛型中的类型擦除表现为以下几点:
- 类型参数擦除:在编译时,泛型类型参数会被擦除为它们的上限边界或者 Object 类型。例如,
List<Integer>
在运行时会变成List
,List<String>
也会变成List
。 - 类型转换:泛型类型的实例化会被转换为原始类型。例如,
ArrayList<String>
在运行时会被转换为ArrayList
。 - 类型检查:编译器会在编译时进行泛型类型的类型检查,但在运行时泛型类型信息已经被擦除,无法进行运行时的类型检查。
- 无法直接实例化泛型类型:由于类型擦除,无法直接实例化泛型类型,例如不能实例化
new ArrayList<T>()
,而只能实例化new ArrayList<>()
。
尽管类型擦除提供了向后兼容性,但它也带来了一些局限性:
- 无法在运行时获取泛型类型信息:由于类型擦除,导致在运行时无法获得泛型类型信息,这限制了一些需要在运行时处理泛型类型的操作,如创建泛型类型的实例、动态调用泛型方法等。
- 导致泛型数组的限制:由于泛型类型信息在运行时被擦除,Java 中无法直接创建泛型数组,例如
new T[]
是非法的。这限制了在泛型中使用数组的一些操作。 - 类型转换可能导致 ClassCastException:由于类型擦除,泛型类型在运行时被转换为原始类型,这可能导致在使用泛型时发生类型转换错误,从而引发 ClassCastException 异常。
谈一谈Java成员变量,局部变量和静态变量的创建和回收时机
- 成员变量(实例变量) :
- 成员变量定义在类中,每个对象实例都有自己的一组成员边里。当创建类的实例对象时,每个实例变量都会在堆内存中分配内存空间,并根据其类型进行默认初始化。
- 回收时机:成员变量所占用的内存空间会在对象被垃圾回收器回收时释放,会被标记为可回收的,待下一次垃圾回收时释放。
- 局部变量:
- 创建时机:局部变量是在方法、代码块或构造函数内部定义的变量,它们只在定义它们的作用域内有效。当进入定义变量的代码块时,局部变量会被分配内存空间,并根据其类型进行默认初始化。
- 回收时机:局部变量所占用的内存空间在方法、代码块或构造函数执行完毕后会被释放。当局部变量的作用域结束时,其所占用的内存空间会被回收。
- 静态变量:
- 创建时机:静态变量是使用
static
关键字修饰的成员变量,它们属于类而不是对象实例,只会在类加载时初始化一次。在类加载时,静态变量会在方法区(Java 8之前是永久代,Java 8及以后是元空间)中分配内存空间,并根据其类型进行默认初始化。 - 回收时机:静态变量所占用的内存空间会在类被卸载时释放,即当类加载器卸载类时,静态变量所占用的内存空间会被释放。一般情况下,类被卸载的时机是在程序结束时,或者当类加载器不再引用某个类时。
内部类都有哪些?
- 成员内部类(Member Inner Class) :
- 成员内部类是定义在外部类内部的普通类,可以访问外部类的所有成员,包括私有成员。
- 成员内部类的对象必须依赖于外部类的实例对象存在,因此它们不能定义 static 成员。
- 静态内部类(Static Nested Class) :
- 静态内部类是定义在外部类内部的静态类,可以直接访问外部类的静态成员,但不能访问外部类的非静态成员。
- 静态内部类的对象不依赖于外部类的实例对象存在,因此可以定义静态成员。
- 局部内部类(Local Inner Class) :
- 局部内部类是定义在方法或代码块内部的类,它们只在定义它们的方法或代码块内部可见,外部类无法访问局部内部类。
- 局部内部类可以访问外部类的成员和方法,但是访问外部方法的局部变量必须是 final 或者等效于 final 的(Java 8 之后可以不显式声明为 final,但其值不能被修改)。
- 匿名内部类(Anonymous Inner Class) :
- 匿名内部类是一种没有名字的内部类,通常用于创建实现某个接口或者继承某个类的临时对象。
- 匿名内部类在定义的同时创建了对象,并且通常是在方法参数或者方法内部使用,用于简化代码结构。
什么是多态?
多态(Polymorphism)是面向对象编程中的一个重要概念,指的是同一操作作用于不同的对象上时,可以产生不同的行为。简而言之,多态允许将子类对象视为父类对象使用,从而提高代码的灵活性、可扩展性和可维护性。
假设我们有一个图形类 Shape
,它有一个 draw()
方法用于绘制图形。然后我们有两个子类 Circle
和 Rectangle
分别表示圆形和矩形。当我们绘制的时候,通过传入的不同的子类对象会覆盖父类的draw()
方法,分别输出"绘制圆形" 和 "绘制矩形"。 这就是多态的体现,相同的方法调用在不同对象上产生了不同的行为。
类加载过程
加载-验证-准备-解析-初始化
-
加载:是把class字节码文件从各个来源通过类类加载器皿装载到内存中。类加载器是当程序需要某个类时,虚拟机会将对应的class文件进行加载,创建出对应的Class对象。而这个将class文件加载到虚拟机内存的过程,便是类加载。
-
验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
-
准备:主要是为了类变量(不是实例变量)分配内存,并且赋予初值。 类变量和实例变量区别:类变量是所有对象共有,一个对象改变了值,其余对象的到的都是改变过的对象。实例变量是对象私有的,某一个对象改变了值,不影响其他对象。
-
解析:将常量池内的符号引用替换为直接引用的过程。
-
初始化:主要是对类变量的初始化,是执行类构造器的过程。
栈和堆有什么区别?
Java 中的栈和堆属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
程序运行时,内存到底是如何进行分配的?
很多人将Java的内存分为堆内存(heap)和栈内存(Stack),这种划分其实并不完全准确。Java的内存划分实际比这复杂,下图描述一个 HelloWord.java
加载到内存中的过程
- 1 HelloWord.java文件首先需要经过编译器编译,生成HelloWord.class字节码文件。
- 2 Java程序中访问HelloWord这个类时,需要通过ClassLoader(类加载器)将HelloWord.class加载到JVM内存中
- 3 JVM中的内存可以划分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区
程序计数器: 程序计数器是虚拟机中一块比较小的内存,主要用于记录当前线程执行的位置。
当某一个线程被CPU挂起时,需要记录代码已经执行到的位置,在重新执行此线程时,知道从哪行指令开始执行。
虚拟机栈: 一个线程就相当于创建了一个虚拟机栈,例如我们最熟悉的main方法启动,就启动了一个虚拟机栈。
本地方法栈: 本地方法栈主要存储一些局部变量,程序状态等。
堆: 一般存储一些实例化(也就是new)出来的对象。
方法区: 主要存储一些类信息,常量,静态变量等。
ClassLoader的加载机制
一个完整的程序是由多个 .class 文件组成的。在程序运行过程中,需要将这些文件加载到JVM中才可以使用。类加载器(ClassLoader)就负责这些.class的加载。
Java 中的类何时被加载器加载?
程序启动时,并不会一次性加载程序中所有的.class文件,而是在程序运行过程中,动态加载相应的类到内存中。
通常情况下,Java程序中的.class 文件会在以下2种情况下被ClassLoader加载到内存中。
- 调用类的构造方法,也就是new对象时
- 调用类中的静态(static)变量或静态方法时
Java中的三种类加载器(ClassLoader):
- 启动类加载器 BootstrapClassLoader
- 扩展类加载器 ExtClassLoader(JDK 1.9 之后改名为:PlatformClassLoader)
- 系统加载器 APPClassLoader
Java的volatile关键字
在Java中,所有的变量都是放在主内存中的,而每一个线程都有自己的工作内存(高速缓存,工作内存效率更高),线程在运行过程中,需要将主内存中的数据拷贝到工作内存,等数据操作更新完毕之后再更新到主内存中。
主内存认为是堆内存,工作内存是栈内存
主内存中的一个值在并发运行时可能出现线程 B 所读取到的数据是线程 A 更新之前的数据。 显然这样肯定是有问题的,这时候 volatile 的作用就出现了:
当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让工作内存中的该变量的线程中的数据清空,必须从主内存重新读取最新数据。
volatile 修饰的共享变量具有以下两点特性:
1. 保证了不同线程对该变量操作的内存可见性。
2. 禁止指令重排序。
类加载的双亲委托机制
解释定义
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。这种就是双亲委派机制。
简单来说,就是每个儿子都不愿干活,每次有活就丢给父亲去干,直到父亲说这事我干不了,需要你自己动手,这时候儿子才会自己想办法完成。
好处
-
避免重复加载 Java类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载过该类时,就没必要子ClassLoader再加载一次。
-
防止核心API被随意篡改 Java核心API中定义的类型不会被随意替换。假设通过网络传递一个名为
java.lang.Integer
的类,通过双亲委派机制传递到启动类加载器,而启动类加载器的核心Java API
发现这个名字的类已被加载,并不会重新加载网络传递过来的java.lang.Integer
, 而是直接返回已加载过的Integer.class
,这样便可以防止核心API库被随意篡改。
Java的强引用、软引用、弱引用和虚引用
Java 判断对象 GC 的方式有两种,一种是 引用计数法
,另外一种是 可达性算法
。
引用计数法:Java堆中每一个对象都有一个引用计数属性,引用每新增1次计数器则+1,引用每释放一次计数则-1。
可达性分析法:判断对象的引用链是否可达来决定对象是否被回收。
从 JDK1.2
版本开始,对象的引用被划分为4
种等级,从而使程序能更灵活的控制对象的生命周期
。这4
种级别由高到低依次为:强引用、软引用、弱引用及虚引用。
1.强引用(StrongReference)
强引用时使用最普遍的引用,如果一个对象是强引用,则GC一定不会回收它。最简单的例子:
Object strongReference = new Object();
如上代码,当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError
错误使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
当内存空间不足时,Java
虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果强引用对象不使用时,需要弱化从而使GC
能够回收,如下:
strongReference = null;
2.软引用(SoftReference)
如果一个对象只具有软引用,则内存空间充足时,垃圾回收就不会回收它,如果内存空间不足了了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存。
// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);
软引用可以和一个 引用队列(RefrenceQueue
) 联合使用。如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// GC回收,软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用
System.gc();
System.out.println(softReference.get());
// abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); //null
当内存不足时,JVM
首先将软引用中的对象引用置为null
,然后通知垃圾回收器进行回收:
if(JVM内存不足) {
// 将软引用中的对象引用置为null
str = null;
// 通知垃圾回收器进行回收
System.gc();
}
应用场景:
浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
- 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
- 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
这时候就可以使用软引用,很好的解决了实际的问题:
// 获取浏览器对象进行浏览
Browser browser = new Browser();
// 从后台程序加载浏览页面
BrowserPage page = browser.getPage();
// 将浏览完毕的页面置为软引用
SoftReference softReference = new SoftReference(page);
// 回退或者再次浏览此页面时
if(softReference.get() != null) {
// 内存充足,还没有被回收器回收,直接获取缓存
page = softReference.get();
} else {
// 内存不足,软引用的对象已经回收
page = browser.getPage();
// 重新构建软引用
softReference = new SoftReference(page);
}
3.弱引用(WeakReference)
弱引用的对象,在垃圾回收器线程扫描内存区域
时,一旦发现具有弱引用的对象,不管当前内存控件足够与否,都会回收内存
。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
输出结果:
hello
null
第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。
4.虚引用(PhantomReference)
虚引用,顾名思义就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何地方都坑被垃圾回收器回收。
用一张表格说明就是:
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强 引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
弱 引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
软 引用 | 正常垃圾回收 | 对象缓存 | 垃圾回收后终止 |
虚 引用 | 正常垃圾回收 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
垃圾回收 和 分代回收策略
所谓垃圾
是指在内存中没有用的对象,既然是垃圾回收
,就必须知道那些对象是垃圾。
Java虚拟机中使用一种叫做可达性分析
的算法来决定对象是否可以被回收
。
如何确定一个对象是否可以被回收?
- 引用计数法
引用计数法是早期的一种计算策略,通过判断对象的引用数量来决定对象是否可以被回收。
在这种方法中,堆中的每个对象实例都有一个引用计数
。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为1
。当任何其他变量被赋值给这个对象的引用时,对象实例的引用计数器加 1
(a = b,则b引用的对象实例的计数器加 1
),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1
。特别地,当一个对象实例被垃圾回收时,它引用的任何对象实例的引用计数器均减 1
。任何引用计数为0的对象实例都可以被当作垃圾回收。
- 可达性分析法
可达性分析法是判断对象的引用链是否可达来决定对象是否被回收。
可达性分析算法是从离散数学中引入的。JVM把内存中所有对象之间的引用关系看作是一张图,通过一组名为GC Root
的对象作为起点,从这些节点向下搜索,搜索所过的路径被称为引用链
,最后通过判断对象的引用链
是否可达来决定对象是否被回收,如下图所示:
上图中,对象A、B、C、D、E与GC Root之间存在一条直接会间接的引用链,这也说明他们与GC Root是可达的,因此他们是不能被GC回收的。而对象M、K虽然被J引用,但是与GC Root并未通过引用链链接,所以当GC进行垃圾回收时,遍历到J、K、M对象时就会被回收
1. 重写和重载的区别?
1. 参数列表:
- 重载方法的方法名相同,参数不同。
- 重写方法的参数列表必须与被重写方法相同。
2. 方法体:
- 重载方法的方法体可以不同。
- 重写方法的方法体通常提供了新的实现。
3. 编译时与运行时:
- 重载是在编译时确定的。
- 重写是在运行时确定的,根据对象的实际类型来调用相应的方法。
总的来说,重载用于在同一个类中提供多个同名方法,以处理不同类型的参数;而重写用于在子类中提供对父类方法的新实现,以实现多态性。
并发和并行的区别?
并发: 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并行: 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
比如两个人在吃午饭。A在吃饭的整个过程中,吃了米饭、吃了蔬菜、吃了牛肉。吃米饭、吃蔬菜、吃牛肉这三件事其实就是并发执行的。对于A来说,整个过程中看似是同时完成的的。但其实A是在吃不同的东西之间来回切换的。
还是两个人吃午饭。在吃饭过程中,A吃了米饭、蔬菜、牛肉。B也吃了米饭、蔬菜和牛肉。AB两个人之间的吃饭就是并行的。两个人之间可以在同一时间点一起吃牛肉,或者一个吃牛肉,一个吃蔬菜。之间是互不影响的。
2. HashMap 的工作原理
Java 中的 HashMap
HashMap是基于Hash算法(Hash算法:也叫散列算法,是指任意长度的输入,通过散列算法,变成固定长度输出
)实现的,它利用put(key,value)存储,get(key)获取。当传入Key值时,HashMap会根据key.hashCode()计算出hash值,然后利用hash值将value值存储到Hash桶里。当计算出的hash值相同时,也就是出现hash冲突(Hash冲突:根据key(键)即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经被占用了。这就是所谓的hash冲突。
)时,HashMap的做法是使用链表和红黑树来存储相同的hash值的value。当hash值的冲突较少时,使用链表,否则使用红黑树。
下面我将解释 HashMap
的工作原理以及为什么要增加红黑树。
-
HashMap
内部使用一个数组(称为哈希桶)来存储键值对。数组的每个位置称为Hash桶,每个桶可以存放多个键值对。数组的大小记不清了。 -
当不同的键映射到同一个桶时,会产生哈希冲突。
HashMap
使用的解决哈希冲突的方法是链地址法(Chaining),即在同一个桶中使用链表来存储具有相同哈希值的键值对。 -
HashMap
使用负载因子来控制哈希表的填充程度,当填充程度超过负载因子时,会触发重新哈希操作,即对哈希表进行扩容,并重新将所有键值对分布到新的更大的哈希桶中。
为什么增加红黑树?
在 JDK 8 中,当链表中的元素数量达到一定阈值(默认为 8)时,HashMap
会将链表转换为红黑树,以提高查询、插入和删除操作的性能。这是因为当链表长度过长时,即使使用了哈希表和链表的结构,仍然可能导致性能下降,因为链表的查找效率不高。
红黑树是一种自平衡的二叉查找树,其查询、插入和删除的时间复杂度为 O(log n),相比于链表,红黑树在数据量较大时具有更好的性能表现。因此,当链表中的元素数量较多时,HashMap
会将链表转换为红黑树,以提高性能。
总的来说,增加红黑树是为了优化 HashMap
在处理哈希冲突时的性能,特别是在面对大量元素时,能够保持较高的查询、插入和删除效率。
ArrayList 和 LinkList 的区别
ArrayList
和 LinkedList
都是 Java 中常用的集合类,但它们在内部实现和性能特点上有着显著的区别。
ArrayList
-
ArrayList
基于动态数组实现,底层使用数组来存储元素。 -
ArrayList
支持快速查找,可以通过索引直接访问元素,时间复杂度为 O(1)。但是插入、删除(在中间或头部插入或删除元素时,需要移动其他元素,导致速度慢)速度慢,时间复杂度为 O(n)
LinkedList
LinkedList
基于双向链表实现,每个节点都包含了对前一个节点和后一个节点的引用。LinkedList
快速查找速度慢,访问元素需要从头部或尾部开始遍历链表,时间复杂度为 O(n)。但是插入、删除(在任意位置插入或删除元素时,只需要修改相邻节点的引用,时间复杂度为 O(1)。)速度快,时间复杂度为 0(1)。
Java 的泛型
Java 泛型是JDK1.5引入的一种新特性,是一种参数化类型。参数化类型就是在不创建新类型的情况下,通过泛型指定的类型控制形参限制的类型,允许在编译期检测非法类型。
泛型的特点
- 类型安全。使用泛型定义的参数在编译期可以对一个类型进行验证,从而更快的暴露问题。
- 消除强制类型转换
- 避免不必要的装箱、拆箱操作,提高程序性能
- 提高代码的重用性
泛型可以修饰
泛型可以修饰 类
、接口
以及方法
- 修饰类
修饰符 class 类名<声明自定义泛型> {
...
}
- 修饰接口
修饰符 interface 接口名<声明自定义泛型>{
}
- 修饰方法
修饰符 泛型类型T 方法名(参数){
}
通配符
- 通配符的产生?
通过上图可以看到,Generic<Int>
不能被做成Generic<Number>
,那该如何解决上面的问题?我们总不能定义一个新的方法来处理Generic<Integer>
类型的类,这显然和java的多态理论是相违背的,因此我们需要在一个逻辑上可以同时表示Generic<Int>
和Generic<Number>
父类的引用类型。由此类型通配符就应运而生。
我们可以将上面的方法改一下:
public void showKeyValue(Generic<?> obj) {
}
上边界限定通配符[Java]/协变[Kotln]
-
Java语言利用
<? extends T>
形式的通配符可以实现泛型的向上转型。 -
Kotlin语言利用
<out T>
形式的通配符实现泛型的向上转型:
使用上通配符后编译器为了保证运行时的安全,会限定对其写的操作,开放读的操作
。
下边界限定通配符[Java]/逆变[Kotln]
- Java语言利用
<? super T>
形式的通配符可以实现泛型的向上转型 - Kotlin语言利用
<in T>
形式的通配符实现泛型的向上转型
与上边界通配符相反,下边界通配符通常限定读的操作,开放写的操作
。
无边界通配符[Java]/星投影[Kotln]
- Java:
<?>
- kotlin:
<*>
类型擦除
我们都知道,Java的泛型是伪泛型(伪泛型是因为:在java编译期间,所有的泛型信息都会被擦掉),java的泛型基本上都是在编译器这层实现的,在生成字节码中不包含泛型类型信息.使用泛型的时候加上类型参数,在编译器编译时会去掉,这个过程称为类型擦除
。
例如定义List和List等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的
java 反射
Java 反射是指在运行时动态地获取类的信息以及动态调用类的方法和属性。
使用反射,可以在编译时不知道类的情况下,通过类的名称、方法名称等信息来实现对类的操作。以下是 Java 反射的主要内容:
1. 获取 Class 对象:
- 可以通过对象的
.getClass()
方法或者.class
字面常量来获取 Class 对象,也可以使用Class.forName()
方法通过类名来获取 Class 对象。
Class<?> clazz1 = MyClass.class;
Class<?> clazz2 = myObject.getClass();
Class<?> clazz3 = Class.forName("com.example.MyClass");
2. 获取类的信息:
- 通过 Class 对象,可以获取类的名称、包信息、修饰符、父类、接口、字段、方法等信息。
String className = clazz.getName();
Package packageInfo = clazz.getPackage();
int modifiers = clazz.getModifiers();
Class<?> superClass = clazz.getSuperclass();
Class<?>[] interfaces = clazz.getInterfaces();
Field[] fields = clazz.getDeclaredFields();
Method[] methods = clazz.getDeclaredMethods();
3. 创建对象:
- 可以使用
newInstance()
方法创建类的实例,前提是类必须具有无参构造方法。
Object instance = clazz.newInstance();
4. 调用方法:
- 可以使用
getMethod()
或者getDeclaredMethod()
方法获取方法对象,然后通过invoke()
方法调用方法。
Method method = clazz.getMethod("methodName", parameterTypes);
Object result = method.invoke(instance, args);
5. 访问字段:
- 可以使用
getField()
或者getDeclaredField()
方法获取字段对象,然后通过get()
、set()
方法访问字段的值。
Field field = clazz.getField("fieldName");
Object value = field.get(instance);
field.set(instance, value);
6. 动态代理:
- 反射也可以用于创建动态代理,通过
Proxy.newProxyInstance()
方法创建代理对象,可以在运行时动态处理方法调用。
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[] { MyInterface.class },
new MyInvocationHandler());
使用反射需要注意性能问题,因为反射操作会消耗较多的系统资源,而且容易导致类型安全性问题。因此,在合适的情况下使用反射是有必要的,但是应该尽量避免过度使用。
Java注解
1. 简要介绍注解的概念和作用:
- 注解(Annotation)是 Java 语言的一种元数据,用于为代码提供额外的信息和描述。
- 注解可以用于配置、描述、检查和处理程序代码,提高了代码的灵活性和可维护性。
2. 常见的注解有三种,分别是内置注解、元注解以及自定义注解:
- 内置注解:内置注解主要是Java内部定义的一些注解,如
@Override
、@Deprecated
、@SuppressWarnings
等,以及它们的作用和用法。 - 元注解:元注解就是给注解使用的注解,如
@Retention
、@Target
、@Documented
、@Inherited
等,以及它们用于定义和处理自定义注解的方式。 - 自定义注解:我们可以自定义一些注解。
注解主要有四个部分的作用:
- 生成文档:即将元数据生成为Javadoc文档;
- 编译检查:编译器在编译阶段会对代码进行检查,例如@override注解会提示编译器查看其是否重写了父类的方法;
- 编译动态处理:主要是用作动态生成代码,例如一些帮助类、方法,通过注解实现自动生成(比如:Retrofit);
- 运行动态处理:典型的例子是使用反射来注入实例(比如:Dagger以及Hilt);