面试──java基础题
java基础题
Java中的序列号和反序列化
- 序列号是指的是将对象转换为字节流的过程,这样对象就可以通过网络传输、持久化存储或者缓存。Java提供了
Serializable接口来支持序列化。只要实现这个接口,就可以将对象序列化。 - 反序列化是将字节流转换为Java对象的过程,即从存储中读取数据并重新创建对象。
- 序列化无法存储静态变量的原因是因为,静态变量属于类,与类的定义相关联。更具体的说是序列化通过调用对象的
writeObject方法和readObject方法来将对象写入输出流或者输入流,而静态变量由于不属于对象的这一部分,因此调用这两个方法时静态变量都不参与其中。所以说静态变量不参与序列化。
Java中Exception和Error有啥区别
Exception和 Error都是 Throwable类的子类(在Java代码中只要继承了 Throwable类的实例才可以被thow或者被catch。它们表示程序在运行的时发生的异常或错误情况。
总结来看就是说 Exception是可以被处理的异常情况,Error是系统级别的不可恢复的错误。
Exception又分为 checkedException和 UncheckedException 。前者为编译时异常,后者是运行时异常。前者必须在代码中显式的throw或者try-catch。后者不需要捕获异常。
Java的优势是啥
从跨平台、垃圾回收、生态、面向对象四方面来阐述。
-
跨平台:
首先Java是跨平台的,不同平台执行的机器码是不一样的。而Java是由于中间加了一层JVM,所以可以做到一次编译多平台运行。。编译的过程主要是把Java的源代码编译为class字节码,字节码再由JVM解释或者JIT进行编译。而因为JIT编译是需要预热的,所以还提供了AOT,可以直接把字节码编译为机器码。来让程序重启之后迅速拉满战斗力
-
垃圾回收
Java还自带垃圾回收的功能。虽然说手动管理内存意味着自由、精细化的掌握。但是很容易出错。在内存比较充足的情况下,把内存管理交给GC来做减轻程序员负担,提升开发效率。
-
生态
Java的生态简直夸张,基本处于无敌的存在。30年的沉淀是很难一下子超越的。
-
面向对象
Java是一门严格面向对象的编程语言,具有清晰的类、对象、继承、接口等概念,支持封装、继承、多态等OOP特性,有助于代码的可拓展性和可维护性。
什么是Java的多态特性
多态是指同一个接口或者父类引用变量指向不同的对象实例,并根据实际指向的对象类型执行相对应的方法。
他允许同一个方法在不同对象上有不同的行为,面向对象编程的核心特性之一。
多态的优点:
- 通过多态程序可以灵活的处理不同类型的对象,降低代码的耦合度。增强系统的可扩展性。新增子类或实现类的时候,无需修改原有代码,需要通过接口或者父类调用即可。
编译时多态:
- 编译时多态也叫静态多态。是编译阶段确定的方法调用。编译时多态主要通过方法重载来实现。
- 方法重载,指的是一个类中定义多个方法,但这些方法的名称相同,但参数列表不相同。Java编译器会根据方法调用传入的参数类型和数量来决定调用哪一个重载方法。
运行时多态:
- 运行时多态也叫动态多态,是在运行的时候确定方法调用。运行时多态通过方法重写实现。
- 方法重写:子类重写父类的一个或者多个方法,通过父类引用调用方法,实际执行的是子类重写后端方法。这种多态特性是在运行的时候根据对象的实际类型来决定的。
Java中参数传递是按值传递还是按引用
在Java中,参数传递只有按值传递,不论是基本类型还是引用类型。
- 级别数据类型:传递的是值的副本,即基本类型数值本身。因此,对方法参数的任何修改都不会影响原始变量。
- 引用数据类型:传递的是引用的副本,即对象引用的内存地址。因此,方法内可以通过修改引用对象的属性,但不能改变引用本身,使其指向另一个对象。
Java中为啥不可以多继承
主要原始是多继承可能会造成菱形继承的问题。Java就是吸收C++的教训,因此不允许多继承。

比如如图所示,如果D要使用定义在A中的方法,那么BC可能都对方法进行了重写,那么调用的时候,不清楚调用的是那个的方法。
为啥又支持实现呢?
在Java8之前接口是无法定义具体的实现类的,所以即使有多个接口必须子类自己实现,所以不会发生歧义。Java8之后接口中有默认方法,Java就强制规定子类必须重写这个默认方法。不如编译过不了。
面向对象和面向过程编程的区别
- 面向对象编程,将数据和方法封装为对象,作为程序的基本单元来组织代码,并运用提炼出封装继承多态作为代码的设计指导,注重代码复用和灵活性。
- 面向过程编程是以过程作为基本基本单元来组织代码的。过程应用到代码中就是函数将函数和数据分离,更关注步骤和流程其实就是一条道的思路,关心如何设计出一系列顺序的执行的过程实现。
相对来说OOP更加适合扩展性的复杂系统的开发,而面向过程更适合简单任务是实现。
Java方法重载和方法重写的区别
方法重载
在一个类中,允许有多个同名方法,它们的参数列表不相同(参数格式,参数类型,参数顺序)。主要关注方法的签名变化,适合同一类中定义不同场景的行为。
方法重写
子类在继承父类的适合,可以重写父类的方法(参数列表,方法名必须相同),从而为该方法提供新的实现。主要关注的是继承关系,用于子类改变父类的方法实现,实现运行时的多态。
区别如下表所示:
| 区别 | 重载 (Overloading) | 重写 (Overriding) |
|---|---|---|
| 发生的场所 | 在同一个类中 | 在继承关系的子类和父类之间 |
| 参数列表 | 必须不同 (参数的数量、类型或顺序不同) | 必须相同,不能改变参数列表 |
| 返回类型 | 可以不同 | 必须与父类的返回类型相同,或者是父类返回类型的子类 (协变返回类型) |
| 访问修饰符 | 不受访问修饰符影响 | 子类方法的访问修饰符不能比父类更严格,通常是相同或更宽泛 |
| 静态和非静态方法 | 可以是静态方法或非静态方法 | 只能重写非静态方法,静态方法不能被重写 (静态方法可以被隐藏) |
| 异常处理 | 方法的异常处理可以不同 | 子类的异常不能抛出比父类更多的异常 (可以抛出更少的或相同类型的异常) |
重载注意点:重载的同名不同参,但是和返回值以及返回类型是没有关系的。
重写注意点:子类方法不能有比父类更严格的访问级别。可以扩大或不变,不能缩小。
子类抛出的异常级别不能比父类大,可以相同或更小。
Java的内部类
Java的内部类是指的是一个类的内部定义的类,Java支持多种类型的内部类,包括成员内部类、局部内部类、匿名内部类以及静态内部类。内部类可以访问外部成员变量和方法,甚至包括私有成员。
内部类的作用:
- 封装性:将逻辑相关的类封装在一起,提高类的内聚性。
- 访问外部成员:内部类可以方便的访问外部类的成员变量和方法,尤其在操作外部类对象的场景非常有作用。
- 简化代码:对于只在一个地方使用的小类,内部类可以减少代码的冗余,简化结构。
- 事件处理:匿名内部类广泛用于实现回调函数或者事件监听,简化代码结构,特别是对于实现接口或者抽象类的场景。
- 成员内部类:非静态类,作为外部类的一个成员。它可以直接访问外部类的所有成员,包括私有成员。
- 静态内部类:定义为
static,无法访问外部类的非静态成员。只能访问外部类的静态成员。- 局部内部类:定义在方法或者代码块中的类仅在该方法创建短期使用的类示例,尤其是在接口或回调事件处理的时候被广泛使用。
实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后内部类会提升为外部的顶级类,和外部类没有任何区别,所以在JVM里面是没有内部类的概念的。
Java8新特性
- 用元空间替代永久代:因为永久代回收效率太低。
- 引入lambda表达式:简化匿名内部类的写法
- 函数式接口:仅含一个抽象方法的接口(如Predicate、Function),是lambda表达式的载体。
- 引入日期类:替代了线程不安全的Date和Calendar,提供了LocalDate、LocalDateTime等不可变类。
- 接口默认方法、静态方法:允许接口使用default关键字定义方法实现、解决接口升级时对实现类的兼容性问题(如Collection新增Stream()方法)
- 新增Stream流式接口:提供声明式集合处理能力,通过链式调用实现过滤、映射、排序等操作。支持并行流(ParallelStream())利用CPU多核加速处理。
- 引入Optional类:封装可能内null的值,提供orElse(),ifPresent()等方法优雅处理null值。避免空指针异常。
- 新增CompletableFuture、StampedLock等并发实现类。
Java11新特性
- 标准化HTTP客户端API:替代老旧的
HttpURLConnection,原生支持HTTP/2、WebSocket及同步异步的请求,无需再依赖Apche HttpClient等第三方依赖库,API涉及更简洁。 - String类的使用方法增强:新增
isBlank()(判断空/空白)、strip()(处理Unicode空白)、lines()(多行转换为Stream)repeat()(重复字符串),解决日常字符串处理痛点。 - Files类简化文件读写:通过
writeString()/readString()实现一行代码读写文件,自动处理流关闭和字符编码,替代传统的FileReader/FileWriter繁琐模版 - Optional新增isEmpty():补充
ifPresent()的反向判断能力,空值检查更直观。
Java17新特性
Java17的核心特点是聚焦在代码设计安全性和JDK稳定性。
- Sealed密封类:打破了类要么完全继承、要么完全禁止继承的限制,可以精确指定允许继承的子类。通过
permists关键字被允许的子类需要显示声明继承策略(final/sealed/non-sealed),配合Switch模式匹配可以实现类型穷举检查,避免遗漏分支。 - 增强的随机数生成器:新增
RandomGenerator接口,支持多种随机数算法(如L32X64MixRandom),性能优于传统的Random,且算法选择更灵活。 - 强封装JDK内部API:彻底禁止通过反射访问JDK内部如(
sun.misc.Usafe、jdk.internal.*),提升JDK安全性和稳定性,但需要迁移依赖内部API的代码。
Java21新特性
- Vitual Thread 虚拟线程:Java并发编程革命性特性,轻量级线程,仅仅几KB内存。支持百万级创建;阻塞时自动让出底层的OS线程,无需手动处理异步逻辑,然写代码写法和普通线程一致大大幅度降低并发编程的门槛
- Switch模式匹配:突破传统的switch仅可以匹配常量的性质,支持直接根据对象类型匹配并绑定变量(如
case String text -> 。。。),还可以结合when添加条件判断,代替繁琐的instanceof嵌套,代码更简洁。 - Record模式:针对Record数据类的结构能力,可以一次性提取Record嵌套字段(如
case Employee(Person(var name,var age),...)->...)同时完成类型检查、数据提取和条件判断,简化数据处理逻辑。 - 分代ZGC:在ZGC的超低延迟继承上引入分代设计,将堆分为年轻代(频繁搜集朝生夕死对象)和老年代(低频率收集长期存活的对象),兼顾低延迟于内存分配密集场景的效率。
Java中的String、StringBuilder和StringBuffer区别
他们都是处理字符串的类,区别主要在可变性、线程安全性和性能上:
-
String:
- 不可以变:String是不可变的,字符串一旦创建,其内容无法修改。每次堆String进行修改操作,都会创建新的String对象。
- 适合场景:String适合于字符串的内容不会频繁变化的场景,例如少量的字符串拼接操作或者字符串常量。
-
StringBuilder:
- 可变:StringBuilder是可变的,提供了与StringBuffer类似的接口。
- 非线程安全:StringBuilder不保证线程安全,性能比StringBuffer更高。
- 适合场景:StringBuilder适合用于单线程环境中需要大量修改字符串的场景,如高频拼接操作。
-
StringBuffer:
- 可变:StringBuffer是可变的,,可以进行字符串的追加、删除、插入等操作。
- 线程安全:StringBuffer是线程安全的,内部使用类
synchronized关键字来保证多线程环境下的安全性。 - 适合场景:StringBuffer适用于多线程环境下需要频繁修改字符串的场景。
总结:
- String:不可变类,适合少量字符串操作。
- StringBuffer:可变且线程安全,适合多线程环境下的频繁字符串修改。
- StringBuilder:可变且线程不安全,适合单线程环境中堆字符串。字符串的处理。
Java中StringBuilder是如何实现的。
底层是使用的char[]数字实现的,和String不同的是,String的char[]数组被final和private修饰,String本身也被final修饰,是典型的immutable类。
由于数组是连续的内存结构,为了频繁的复制和申请内存,需要提供capacity来(默认大小是16)设置初始化容量。这样在已知大字符串的情况下,可以减少数组的扩容次数。
关于append方法:调用的是父类AbstractStringBuilder的append方法。操作步骤如下:
- 先判断是否是特殊值,如果是特殊值,直接处理。
- 然后计算int转换为字符需要几位字符。
- 判断数组够不够长,不够长就扩容。
- 把int转换为字符,塞到数组中。
- 更新现有字符数。等待下次添加。
然后看一下扩容的 ensureCapacityInternal方法:
其实就是直接 Arrays.copyOf进行一波扩容加拷贝,扩容之后的数组容量为以前的两倍+2。
通过Integer.stringSize方法,可以获取该int所占字符位数。该方法在Integer里面。其实这里面用的是查表法。直接列出了各个位数的边界值,一次放入数组中,然后判断大小再根据数组下标算出位数。
insert方法就没啥特别的了。先判断插入的正确性。然后判断是否 需要扩容。然后移动数组里的位置。使用的是System.arraycopy方法。
注意:原生的StringBuilder并没有实现缩容的操作,并且底层使用的是char数组而非byte数组+coder标志位。所以还可以优化。
Java中的包装类型和基本类型
基本类型:基本类型一共8种。int、long、float、double、char、byte、``boolean 、short`,他们都是直接存储数值的变量,在栈内存上(局部变量在栈上,成员变量在堆上,静态字段在方法区),性能较高,且不支持null。
包装类型:每个基本类型都有一个对应的保证类型。包装类型是类,在堆里,可以用于面向对象编程。并且支持null。
总结:
- 性能区别:
- 基本类型:占用内存少,效率高,适合频繁使用的简单操作。
- 包装类型:因为是对象,涉及内存分配和垃圾回收,性能相对较低。
- 比较方式不同:
- 基本类型:比较是直接使用
==,直接比较数值。 - 包装类型:比较时,
==是比较的是对象的内存地址。而equals(),比较的是对象的值。
- 基本类型:比较是直接使用
- 默认值不同:
- 基本类型:默认是0,flase等
- 包装类型:默认是null
- 初始化方式不同:
- 基本类型:直接赋值
- 包装类型:需要采用new的方式。
- 存储方式不同:
- 基本类型:如果是局部变量:则存在栈上,如果是成员变量则存在堆中。
- 包装类型:保存在堆上,(成员变量,在不考虑JIT优化的栈上分配时,都随着对象一起保存在堆上的)。
自动装箱与拆箱
Java中的抽象类和接口有啥区别
接口和抽象类的设计动机不同,接口的设计是自上而下的。我们知晓某一行为,于是基于这些行为定义了接口,一些类需要这些行为,因此实现对应的接口,
抽象类的设计是自下而上的。我们写了很多类,然后发现它们有许多共性之处,有些代码是可以复用的,因此将公共逻辑封装为一个抽象类,减少冗余的代码。
所谓自上而下,是指先约定接口然后实现,自下而上是先有一些类然后才抽象共同父类。
其他区别:
-
方法实现
接口中方法默认是public 和abstract,Java8之后有default方法和静态方法。而抽象类可以有abstract方法和具体方法。它允许子类继承并重用抽象类中的方法实现。
-
构造函数和成员变量
接口不能有构造函数,接口中的成员变量默认是public static final的即常量。抽象类可以包含构造函数,成员变量可以有不同的访问修饰符,并且可以不是常量。
-
多继承
抽象类只能单继承,接口可以多是实现。
JDK和JRE的区别
JRE是Java的运行时环境,包含类JVM、核心类库和其他支持运行的Java程序文件。
JDK可以视为JRE的超集,它包含类JRE并且还有一系列的开发工具,和附加库和文件。
Java中hashcode和equals方法是啥?它们与==操作符的区别是啥
hashcode、equals、==都是Java中比较对象的三种方式,但是它们的用途和实现还是有很大区别的,
hashcode用于散列存储结构中确定对象的存储位置。可用于快速比较两个对象是否相同。因为它们的哈希码不同,那么它们肯定不同。equals用于比较两个对象的内容是否相同,通常需要重写自定义比较逻辑。==用于比较两个引用是否指向同一个对象(即比较的是内存地址)。对于基本数据类型它比较的是值
Java中的hashcode和equals有啥关系
在Java中 hashcode和 equals()方法的关系主要体现在集合类中。它两决定了对象是否是逻辑相等性和哈希存储方式
-
equals()方法用于判断两个对象是否相等,默认实现是使用
==来实现的,即比较对象的内存地址。,但可以在类中重写equals()方法来自定义逻辑。 -
hashcode()方法返回对象的哈希值,主要用于基于哈希的集合。同一个对象每次调用
hashcode()返回的值必须相同。且相等的对象必须有相同的哈希码。
两者关系
- 如果两个对象根据
equals()相等,它们的hashcode()值必须相等。即a.equals(b)==true那么必有a.hashcode()==b.hashcode()必须为true。 - 但是反过来不成立,因为可能有哈希碰撞。
什么是Java中的动态代理
Java中的动态代理是一种在运行时创建代理对象的机制,动态代理允许在运行时决定代理对象的行为,而不需要编译时确认。它通过代理模式为对象提供一种机制,使得可以在不修改目标对象的情况下对其行为进行增强或者调整。
代理可以看作是调用目标的一个包装,通常用来调用真实的目标之前进行一些逻辑,消除一些重复的代码。
静态代理指的是我们预先编码好的一个代理类,而动态代理是运行时生成的代理类。
- Java动态代理:只可以对接口进行代理,不支持对类进行代理。
- CGLIB代理:通过字节码技术动态的生成目标类的子类来实现代理,支持对类(非接口)进行代理。
JDK动态代理和CGLIB代理有啥区别
JDK动态代理是基于接口的,所有的代理类一定要有定义接口,通过实现InvocationHandler接口进行方法增强操作,通过Proxy创建代理对象。
CGLIB是基于ASM字节码生成工具。它是通过继承方式生成目标的子类来实现代理类,所以要注意fianl方法。
Java中的注解原理是啥
注解其实是一个标记,是一种提供元数据的机制,用于给代理添加说明信息,可以标记在类上、方法上、属性上,标记本身可以设置一些值。
注解本身不影响程序的运行逻辑执行,但可以通过工具或者框架来利用这些信息来进行特点的处理。入代理生成编译时检查、运行时间等等。
元注解,即注解的注解:如 @Retention、@Target、@Inherited(表示注解可以继承)
注解的三大保留策略:
@Retention:定义在注解的保留策略,即注解的有效范围。
RetentionPolicy.SOURCE:注解仅停留在源代码中,编译时被丢弃RetentionPolicy.CLASS:注解存在于编译之后的.class文件中,但运行时可不用。RetentionPolicy.RUNTIME:注解在运行时可用,可以通过反射机制来访问注解。
@Target:用来表示注解可以用在哪些代码元素上。
Java中的放射机制,如何进行反射
Java的反射机制是指在运行时获取类的结构信息,(如方法、字段、构造函数等)并操作对象的一种机制。反射机制提供了运行时的动态创建对象、调用方法访问字段等功能。而无需在编译时就知道这些类的具体信息。
反射的使用:
-
获取
class对象:Class<?> clazz = Class.forName("com.mianshiya.MyClass"); // 或者 Class<?> clazz = MyClass.class; // 或者 Class<?> clazz = obj.getClass(); -
创建对象
Object obj = clazz.newInstance(); // 已过时 Constructor<?> constructor = clazz.getConstructor(); Object obj = constructor.newInstance(); -
访问字段
Field field = clazz.getField("myField"); field.setAccessible(true); // 允许访问 private 字段 Object value = field.get(obj); field.set(obj, newValue); -
调用方法
Method method = clazz.getMethod("myMethod", String.class); Object result = method.invoke(obj, "param");
什么是Java的不可变类
不可变类是指在创建之后其状态(对象的字段)无法被修改的类。一旦对象创建成功,它的所有属性都不能被更改,这种类的实例在整个生命周期保持不变。
关键特征:
- 声明类为
final,防止子类继承 - 类的所有字段都为
private、final修饰,确保它们在初始化之后不被更改。 - 通过构造函数初始化所有字段
- 不提供修改对象状态的方法(
setter方法) - 如果类包含类可变对象的引用,确保这些可变对象的引用在对象外部无法被修改。例如getter方法中但会返回对象的副本,new 一个新对象。来保护可变对象。
Java中经典的不可变类:String、Integer、BigDecimal、LocalDate等。
不可变类的优点:
- 线程安全,由于不可变类的状态无法被修改,它们天生就是线程安全的,并且在并发环境中无需同步。
- 缓存友好:不可变对象可以安全的被缓存和共享。如
String字符串常量池 - 防止状态不一致的情况:不可变类可以有效避免因意外修改对象状态而导致不一样的问题。
什么是Java中的SPI机制
SPI机制是一种插件机制,用于在运行时动态加在服务的实现。它通过定义接口的并提供一种可扩展的方式来让服务的提供者在运行时注入,实现解耦和模块化设计。
Java的泛型的作用是啥
Java的泛型作用是通过编译时检查类型安全,允许程序员编写更通用的和灵活的代码,避免在运行时发生类型转换错误。
总结作用:
- 类型安全:泛型运行在编译时运行类型检查,确保在使用集合或者其他泛型类型的适合,不会出现类型不匹配的问题,减少运行时的类型转换错误。
- 代码重用:泛型可以使代码可以使用于多种不同的类型,减少代码重复,提高可读性和可维护性。
- 消除显式的类型转换:泛型运行在编译时指定类型参数,从而消除运行时需要显式类型转换的麻烦。
为啥说Java的泛型是伪泛型:
我们声明一个String类型的几乎,但是可以通过反射往集合中插入int类型的数据,居然还成功了!
说明在运行时,泛型根本没有起作用,也就是说在运行时JVM获取不到泛型的信息,也会不对其做任何的约束。
Java的泛型擦除
泛型擦除是指的是Java在编译器的编译时把所有的泛型信息给删除的过程,确保与Java1.4之前的版本保持兼容。泛型参数在运行时会被替换为其上界通常是Object,这样一来在运行时,无法获取泛型的实际类型。
作用:泛型擦除确保了Java代码的向后兼容性但它也限制了在运行时对泛型类型的操作。
作用:由于类型擦除,无法在运行时获取泛型的实际类型,也不能创建泛型的类型的数组或对泛型类型的使用instanceof检查。
Java的泛型上下界定符
Java的泛型的上下界定符用于对泛型类型参数的进行范围检查,主要有上界定符,和下界定符。
- 上界定符
- 定义
?extends T表示通配符类型必须是T或者T的子类。 - 作用:运行使用T或其子类作用泛型参数,通常用于读取操作,确保可以读取为T或T的子类对象。
- 定义
- 下界限定符
- 定义
?super T表示通配符类型必须是T或者T的父类。 - 作用运行使用T或者T的父类作为泛型的参数,作用用于写入操作,可以确保安全的向泛型集合中插入T类型的对象。
- 定义
Java中的深拷贝和浅拷贝
深拷贝:深拷贝不仅复制对象本身,还递归复制对象中的所有引用的对象。这样新对象和源原对象完全独立,修改新对象不影响原对象。即包括基本类型和引用类型,堆内的引用对象也会复制一份。
浅拷贝:拷贝只复制对象的引用,而不复制引用指向的实际对象,也就是说浅拷贝创建的是一个新的对象,但它的字段指向的适合原对象的相同内存地址。
深拷贝创建的新对象和原对象完全独立,任何一个的修改都不会影响到另一个。而浅拷贝对象引用数据类型的字段会影响到原对象。因为他们共享相同的引用。

如何实现浅拷贝:
使用 Object.clone()方法是浅拷贝的常见方式。默认情况下,clone()方法只是对对象的字段进行了拷贝,对于基本类型的字段会复制值对于引用类型的字段则复用引用。
如何实现深拷贝:
方式一递归调用 clone()手动实现,方式二,通过序列化和反序列化实现。前者吃操作性能高。后者性能低。
Java中的Integer缓存池
Java的Integer缓存池,是为了提升性能和节省内存。根据实践发现大部分的数据操作都集中在值比较小的范围,因此缓存这些对象可以减少内存分配和垃圾回收的负担,提升性能。在 -128到 127范围内的 Integer对象会被缓存和复用。
原理:
- Java在自动装箱的时候,对于值在
-128到127的int转换为Integer时,若数值在缓存范围内,则返回缓存对象。 - 值比较:用于相同范围内的整数使用一个缓存对象,使用
==可比较他们的内存地址,而不需要使用equals()。但是要注意的是超过缓存范围的Integer对象,==比较的是对象引用,而不是数值。需要值比较必须使用equals()方法。
Java8只好可以通过JVM参数调整缓存池的上限:
java -XX:AutoBoxCacheMax=500
Java的类加载机制
类加载机制是把类加在到JVM里面。把二进制流存储的闪存中,经过一番解析、处理转化成可以用的class类。
二进制流可以来源于class文件,或者通过字节码工具生成字节码或者来源于网络,只要符合二进制流,JVM来者不拒。
类加载过程:
- 加载
- 连接
- 初始化
连接还能拆分为:验证准备解析三个阶段。所以总的来看可以分为五个阶段:
-
加载
将二进制流读入到内存中,生成一个Class对象。
-
验证
主要验证加载进来的二进制流是否符合一定的格式,是否规范,是否符合当前的JVM版本等等之类的验证。
-
准备
为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如int的初始值就是0。
-
解析
将常量池的符号引用化为直接引用。直接引用指的是一个真实的引用,在内存中可以通过这个引用来查找目标。
-
初始化
这时候执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是赋个初值占位。
Java的双亲委派模型
双亲委派模型是Java的一个类加载机制的设计模式之一。他的核心思想是:类加载器在加载某个类时,会先委派给父类加载器去加载,父类加载器无法加载时,才会由当前类自行加载。
工作流程:
- 当一个类加载器试图加载某个类时,先将加载请求向上委派给父类加载器,父类加载器。再向上委派给其它父类,直到根类加载器具。
使用new String("yupi")语句在Java中会创建多少个对象。
会创建一个或者两个字符串对象:
主要有两种情况:
- 如果字符串常量池中不存在字符串对象"yupi"的引用,那么他会堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
- 如果字符串常量池中已存在字符串对象"yupi"的引用,则只会在堆中创建一个字符串对象。
为啥JDK9开始将String的char数组变为byte数组
主要是为了节省空间,提高内存的利用率。
在JDK9之前,String类是基于 char[]实现的,内部是使用的UTF-16编码,每个字符占两个字节。但是如果当前字符仅需要一个字节的空间,就造成类空间的浪费。例如一下 Latin-1的字符用一个字节即可表示。
因此JDK9做了优化采用了byte数组来实现,ASCII字符串通过byte数组存储仅需一个字节,减少了内存占用。
并且引入类coder变量来标识编码方式,这样就可以保证兼容性且减少内存占用。
如果一个线程在Java中被两次调用start(),会发生啥
会报错!因为在Java中一个线程只能被启动一次!所以尝试第二次调用的时候会抛出 IllegalTreadStateException异常。这是因为一旦线程已经开始执行,它的状态就不能回到初始状态。线程的生命周期不允许它从终止状态回到可运行状态。